八月 24

QObject信号与槽案例

import sys
from PySide2 import QtWidgets as qtw
from PySide2 import QtCore as qtc

def window_title_changed_slot(name):
    # 暂时屏蔽所有的信号,防止出现递归调用
    window.blockSignals(True)
    window.setWindowTitle('安静一键宏 - ' + name)
    # 解除信号屏蔽
    window.blockSignals(False)

app = qtw.QApplication()

window = qtw.QWidget(None)
window.resize(800, 800)
window.windowTitleChanged.connect(window_title_changed_slot)
# 触发信号,执行槽函数
window.setWindowTitle('猎人')

# 每次点击的时候print('点你咋地!')
btn = qtw.QPushButton(window)
btn.setText('点我')
btn.clicked.connect(lambda: print('点你咋地!'))

window.show()
sys.exit(app.exec_())
八月 24

QObject信号与槽

import sys
from PySide2 import QtWidgets as qtw
from PySide2 import QtCore as qtc

# destroy信号会携带一个参数,这个参数就是被destroy的对象
def name_changed_slot(name):
    print(f'对象名称改变为{name}')

def destroy_slot(obj):
    print(f'{obj}对象被释放了')

app = qtw.QApplication()

window = qtw.QWidget(None)
window.resize(800, 800)

obj = qtc.QObject(None)

# 连接信号与槽
obj.objectNameChanged.connect(name_changed_slot)
obj.destroyed.connect(destroy_slot)

# 检查信号绑定的槽的数量
# 这里的参数要用SIGNAL配合信号名称获取
# 信号名称不用带上sender的名称,但是后面需要加(),否则返回0个receiver
print(obj.receivers(qtc.SIGNAL('destroyed()')))

# 触发objectNameChanged信号
obj.setObjectName('changed_name')
# 临时阻断信号
obj.blockSignals(True)
# obj.signalsBlocked返回布尔值,检测信号是否被阻断
print(obj.signalsBlocked())
obj.setObjectName('another_changed_name')
# 恢复信号连接
obj.blockSignals(False)

# 取消信号与槽的链接。如果不传参数,测取消所有链接。也可以指定取消链接的槽
obj.objectNameChanged.disconnect()
obj.setObjectName('third_changed_name')

# 删除引用,即触发obj的destroy信号
del obj

window.show()

sys.exit(app.exec_())
八月 24

QObject父子对象的操作案例

import sys
from PySide2 import QtWidgets as qtw

app = qtw.QApplication()

# 当控件没有父控件时,会被自动包装为窗口,增加标题等功能
win_root = qtw.QWidget(None)
win_root.resize(500, 500)

label0 = qtw.QLabel(win_root)
label0.setText('label 0')

label1 = qtw.QLabel(win_root)
label1.setText('label 1')
label1.move(0, 30)

label2 = qtw.QLabel(win_root)
label2.setText('label 2')
label2.move(0, 60)

label3 = qtw.QLabel(win_root)
label3.setText('label 3')
label3.move(0, 90)

btn = qtw.QPushButton(win_root)
btn.setText('button')
btn.move(0, 120)

# 给所有类型为QLabel的子控件设置背景色为cyan
for widget in win_root.findChildren(qtw.QLabel):
    widget.setStyleSheet('background-color: cyan;')

win_root.show()

sys.exit(app.exec_())
八月 23

QObject父子对象的操作

import sys
from PySide2 import QtWidgets as qtw
from PySide2 import QtCore as qtc

app = qtw.QApplication()

window = qtw.QWidget(None)
window.resize(800, 800)

obj1 = qtc.QObject()
obj2 = qtc.QObject()

obj1.setParent(obj2)  # 设置父对象。如果多次设置父对象,以最后设置的为准
obj1.setObjectName('1')
print('parent: ', obj1.parent())  # 获取父对象

print('children: ', obj2.children())  # 获取的所有直接子对象,不递归查找

# findChild只能找到一个子对象。当有多个符合条件的子对象时,返回第一个
# 第一个参数为类型或类型元组,第二个参数为objectName,可以省略,
# 第三个参数为查找选项:qtc.Qt.FindChildrenRecursively(默认选项)
# 或 qtc.Qt.FindDirectChildrenOnly
print('findChild: ', obj2.findChild(qtc.QObject, '1'))

# findChildren能找到所有符合条件的子对象。
# findChildren的三个参数和findChild的三个参数一样
print('findChildren: ', obj2.findChildren(qtc.QObject))

sys.exit(app.exec_())
八月 23

qss初识

首先将新建一个后缀为.qss的样式表

QLabel#notice {
    font-size: 20px;
    color: gray;
    border: 1px solid gray;
    border-radius: 8px;
    }

QLabel#notice[level="normal"] {
    color: green;
    border-color: green;
    }

QLabel#notice[level="danger"] {
    color: red;
    border-color: red;
    }

QLabel#notice[level="info"] {
    color: yellow;
    border-color: yellow;
    }

QPushButton {
    font-size: 20px;
    color: green;
    }

然后在界面中读取文件调用即可:

import sys
from PySide2 import QtWidgets as qtw

app = qtw.QApplication()

# 通过独立的文件全局设置qss样式
with open('2. qss初识.qss', 'r') as f:
    qtw.qApp.setStyleSheet(f.read())

window = qtw.QWidget(None)
window.resize(800, 800)

label = qtw.QLabel(window)
# 设置的名称可以在qss文件当中通过 #objectName的方式选择
label.setObjectName('notice')
# 设置的属性可以在qss文件当中通过[property="value"]的方式选择。
# 注意值要用双引号!
label.setProperty('level', 'normal')
label.setText('安静一键宏')

label2 = qtw.QLabel(window)
label2.setObjectName('notice')
label2.setProperty('level', 'danger')
label2.move(0, 30)
label2.setText('轻松玩魔兽')

label3 = qtw.QLabel(window)
label3.setObjectName('notice')
label3.setProperty('level', 'info')
label3.move(0, 60)
label3.setText('好用还不贵')

btn = qtw.QPushButton(window)
btn.move(0, 90)
btn.setText('我是一个按钮')

## 单独对某个元素设置qss样式
# label.setStyleSheet('font-size: 20px; color:red;')
window.show()
sys.exit(app.exec_())
八月 23

QObject初识

import sys
from PySide2 import QtWidgets as qtw
from PySide2 import QtCore as qtc

app = qtw.QApplication()

window = qtw.QWidget(None)
window.resize(800, 800)

obj = qtc.QObject(None)

obj.setObjectName('测试名称')  # 设置名称
print(obj.objectName())  # 获取名称

obj.setProperty('level', 'low')  # 设置动态属性level的值为low
obj.setProperty('owner', 'shuchun')  # 设置动态属性owner的值为shuchun
print(obj.property('level'))  # 获取动态属性level的值
print(obj.property('owner'))  # 获取动态属性owner的值

print(obj.dynamicPropertyNames())

sys.exit(app.exec_())
八月 21

当PyQt和Python VTK一起使用时出现wglMakeCurrent failed in MakeCurrent(), error: The handle is invalid错误

我使用Pyqt和Python VTK一起编程时,在程序上面加了两个Qvtkwidget,通过python,一个用来显示左边的CT横断面,另一个用来显示CT三维切片,其实就是显示两幅不同的图。然后读取和显示都正常,但是一关闭界面的时候就提示这个错误。折腾好久之后,并不是什么python和qt之间乱七八糟的机制的问题。

解决方案:在UI界面关闭时,加个响应函数。

def closeEvent(self, event):
    self.qvtkWidget2D.Finalize()
    self.qvtkWidget2D_2.Finalize()

这样便可解决弹窗报错的问题。当然只是针对这个问题

七月 28

Pyside2教程:CSV编辑器

import sys
import csv
from PySide2 import QtWidgets as qtw
from PySide2 import QtGui as qtg
from PySide2 import QtCore as qtc

class CsvTableModel(qtc.QAbstractTableModel):
    """CSV表格的模型"""

    def __init__(self, csv_file):
        super(CsvTableModel, self).__init__()
        self.filename = csv_file
        with open(self.filename) as fh:
            csvreader = csv.reader(fh)
            self._headers = next(csvreader)
            self._data = list(csvreader)

    def rowCount(self, parent):
        return len(self._data)

    def columnCount(self, parent):
        return len(self._headers)

    def data(self, index, role):
        """
        用于获取模型中的数据
        :param index: QModelIndex类的实例,我们用到了row()和column()两个方法
        :param role: View获取数据时,会告诉model自己用设个数据干什么
        :return: 对应的数据
        """
        if role in (qtc.Qt.DisplayRole, qtc.Qt.EditRole):
            return self._data[index.row()][index.column()]

    def headerData(self, section, orientation, role=None):
        """
        获取头文件
        :param section: 表头的序号
        :param orientation: qtc.Qt.Horizontal或者qtc.Qt.Vertical,行表头或者列表头
        :param role: View获取数据时,会告诉model自己用设个数据干什么
        :return:
        """
        if orientation == qtc.Qt.Horizontal and role == qtc.Qt.DisplayRole:
            return self._headers[section]
        else:
            return super().headerData(section, orientation, role)

    def sort(self, column, order=None):
        # 在排序之前需要发射layoutAboutToBeChanged信号
        self.layoutAboutToBeChanged.emit()
        self._data.sort(key=lambda x: x[column])
        if order == qtc.Qt.DescendingOrder:
            self._data.reverse()
        # 在排序之后需要发射layoutChanged信号
        self.layoutChanged.emit()

    def flags(self, index):
        return super(CsvTableModel, self).flags(index) | qtc.Qt.ItemIsEditable

    def setData(self, index, value, role=None):
        if index.isValid() and role == qtc.Qt.EditRole:
            self._data[index.row()][index.column()] = value
            self.dataChanged.emit(index, index, [role])
            return True
        else:
            return False

    def insertRows(self, row, count, parent=None, *args, **kwargs):
        self.beginInsertRows(parent or qtc.QModelIndex(), row, row + count -1)
        for i in range(count):
            default_row = [''] * len(self._headers)
            self._data.insert(row, default_row)
        self.endInsertRows()

    def removeRows(self, row, count, parent=None, *args, **kwargs):
        self.beginRemoveRows(parent or qtc.QModelIndex(), row, row + count -1)
        for i in range(count):
            del self._data[row]
        self.endRemoveRows()

    def save_data(self):
        # 这里必须有newline='',否则每行数据后面会多一个空行
        with open(self.filename, 'w', encoding='utf8', newline='') as fh:
            writer = csv.writer(fh)
            writer.writerow(self._headers)
            writer.writerows(self._data)

# 将主程序封装成类
class MainWindow(qtw.QMainWindow):
    def __init__(self):
        # 引入父类的__init__方法
        super().__init__(parent=None)
        # Main UI code goes here
        self.tableview = qtw.QTableView()
        self.tableview.setSortingEnabled(True)
        self.setCentralWidget(self.tableview)

        # menu部分
        menu = self.menuBar()
        file_menu = menu.addMenu('文件')
        file_menu.addAction('打开', self.select_file)
        file_menu.addAction('保存', self.save_file)

        edit_menu = menu.addMenu('编辑')
        edit_menu.addAction('在上方插入', self.insert_above)
        edit_menu.addAction('在下方插入', self.insert_below)
        edit_menu.addAction('删除行', self.remove_rows)

        # End main UI code
        # 显示主窗口
        self.show()

    def select_file(self):
        filename, _ = qtw.QFileDialog.getOpenFileName(
            self,
            '请选择要打开的csv文件',
            qtc.QDir.currentPath(),
            'CSV Files (*.csv) ;; All Files (*)'
        )
        if filename:
            self.model = CsvTableModel(filename)
            self.tableview.setModel(self.model)

    def save_file(self):
        if self.model:
            self.model.save_data()

    def insert_above(self):
        selected = self.tableview.selectedIndexes()
        row = selected[0].row() if selected else 0
        self.model.insertRows(row, 1)

    def insert_below(self):
        selected = self.tableview.selectedIndexes()
        row = selected[-1].row() if selected else self.model.rowCount(None)
        self.model.insertRows(row+1, 1)

    def remove_rows(self):
        selected = self.tableview.selectedIndexes()
        if selected:
            # 注意删除行的时候,如果选中同一行中的多个元素,应该按照删除一行来处理。这里用set方法去除相同的行。
            self.model.removeRows(selected[0].row(), len(set(cell.row() for cell in selected)))

if __name__ == '__main__':
    app = qtw.QApplication()
    main_window = MainWindow()
    sys.exit(app.exec_())
七月 25

Pyside2教程:记事本小程序

import sys
from PySide2 import QtWidgets as qtw
from PySide2 import QtGui as qtg
from PySide2 import QtCore as qtc

# 设置对话框
class SettingsDialog(qtw.QDialog):
    """Dialog for setting the settings"""
    def __init__(self, settings, parent=None):
        super().__init__(parent, modal=True)
        self.setLayout(qtw.QFormLayout())
        self.settings = settings
        self.layout().addRow(qtw.QLabel('<h1>设置</h1>'))
        self.show_warning_cb = qtw.QCheckBox(checked=settings.get('show_warnings'))
        self.layout().addRow('显示警告', self.show_warning_cb)
        self.accept_btn = qtw.QPushButton('Ok', clicked=self.accept)
        self.cancel_btn = qtw.QPushButton('Cancel', clicked=self.reject)
        self.layout().addRow(self.accept_btn, self.cancel_btn)

    def accept(self):
        self.settings['show_warnings'] = self.show_warning_cb.isChecked()
        super().accept()

# 将主程序封装成类
class MainWindow(qtw.QMainWindow):

    # 传入的两个参数分别为公司名和程序名
    settings = {'show_warnings': False}

    def __init__(self):
        # 引入父类的__init__方法
        super().__init__(parent=None)
        # Main UI code goes here
        # 设置centralWidget
        self.textedit = qtw.QTextEdit()
        self.setCentralWidget(self.textedit)

        # 设置statusBar。但是这些代码可以省略,因为QMainWindow的statusBar()方法会自动创建一个状态栏
        # status_bar = qtw.QStatusBar()
        # self.setStatusBar(status_bar)
        # status_bar.showMessage('欢迎来到安静编辑器')
        self.statusBar().showMessage('欢迎来到安静编辑器')
        charcount_label = qtw.QLabel('字符:0')
        self.textedit.textChanged.connect(lambda: charcount_label.setText("字符:"+str(len(self.textedit.toPlainText()))))
        self.statusBar().addPermanentWidget(charcount_label)

        # 设置菜单栏
        menubar = self.menuBar()
        file_menu = menubar.addMenu('文件')
        edit_menu = menubar.addMenu('编辑')
        help_menu = menubar.addMenu('帮助')

        # 给菜单栏添加内容。第一个参数为名字,第二个参数为执行的操作。返回一个QAction
        open_action = file_menu.addAction('打开')
        save_action = file_menu.addAction('保存')
        quit_action = file_menu.addAction('退出', self.destroy)
        edit_menu.addAction('撤销', self.textedit.undo)

        # 更复杂的操作可以自己创建一个QAction,然后添加到某个菜单中
        redo_action = qtw.QAction('重做', self)
        redo_action.triggered.connect(self.textedit.redo)
        edit_menu.addAction(redo_action)
        edit_menu.addAction('字体', self.set_font)

        help_menu.addAction('设置', self.show_settings)

        # 设置工具栏
        toolbar = self.addToolBar('文件')
        # 设置工具栏可以放置的位置
        toolbar.setAllowedAreas(qtc.Qt.TopToolBarArea | qtc.Qt.BottomToolBarArea)
        toolbar.addAction(open_action)
        toolbar.addAction(save_action)

        # 获取系统自带的图标
        open_icon = self.style().standardIcon(qtw.QStyle.SP_DirOpenIcon)
        save_icon = self.style().standardIcon(qtw.QStyle.SP_DriveHDIcon)

        # 设置图标
        open_action.setIcon(open_icon)
        save_action.setIcon(save_icon)

        open_action.triggered.connect(self.openFile)
        save_action.triggered.connect(self.saveFile)

        # 设置dock
        dock = qtw.QDockWidget('替换')
        self.addDockWidget(qtc.Qt.LeftDockWidgetArea, dock)

        dock.setFeatures(qtw.QDockWidget.DockWidgetMovable | qtw.QDockWidget.DockWidgetFloatable)

        # 创建dock中的界面
        replace_widget = qtw.QWidget()
        replace_widget.setLayout(qtw.QVBoxLayout())
        dock.setWidget(replace_widget)

        self.search_text_inp = qtw.QLineEdit(placeholderText='搜索')
        self.replace_text_inp = qtw.QLineEdit(placeholderText='替换')
        search_and_replace_btn = qtw.QPushButton('搜索和替换', clicked=self.search_and_replace)
        replace_widget.layout().addWidget(self.search_text_inp)
        replace_widget.layout().addWidget(self.replace_text_inp)
        replace_widget.layout().addWidget(search_and_replace_btn)
        replace_widget.layout().addStretch()

        help_menu.addAction('关于', self.showAboutDialog)

        # response = qtw.QMessageBox.question(self, 'Beta版本提醒', '目前软件仍在测试阶段,请问您确定要使用吗?')
        splash_screen = qtw.QMessageBox()
        splash_screen.setWindowTitle('安静编辑器')
        splash_screen.setText('内测版本警告')
        splash_screen.setInformativeText('这是非常早期的版本,你确定要使用吗')
        splash_screen.setDetailedText('本软件仅供内部使用,很多功能都是针对特定的软件而设置')
        splash_screen.setWindowModality(qtc.Qt.WindowModal)
        splash_screen.addButton(qtw.QMessageBox.Yes)
        splash_screen.addButton(qtw.QMessageBox.No)
        response = splash_screen.exec()

        if response == qtw.QMessageBox.No:
            self.close()
            sys.exit()

        # End main UI code
        # 显示主窗口
        self.show()

    def search_and_replace(self):
        s_text = self.search_text_inp.text()
        r_text = self.replace_text_inp.text()

        if s_text:
            self.textedit.setText(self.textedit.toPlainText().replace(s_text, r_text))

    def showAboutDialog(self):
        qtw.QMessageBox.about(self, '关于文本编辑器', '这是在Pyside2环境下编写的文字编辑器')

    def openFile(self):
        filename, _ = qtw.QFileDialog.getOpenFileName(
            self,
            '请选择要打开的文件',
            qtc.QDir.homePath(),
            'Text Files (*.txt) ;;Python Files (*.py) ;;All Files (*)',
            'Python Files (*.py)',
            qtw.QFileDialog.DontResolveSymlinks
        )
        if filename:
            with open(filename, 'r', encoding='utf-8') as fh:
                self.textedit.setText(fh.read())

    def saveFile(self):
        filename, _ = qtw.QFileDialog.getSaveFileName(
            self,
            "Select the file to save to…",
            qtc.QDir.homePath(),
            'Text Files (*.txt) ;;Python Files (*.py) ;;All Files (*)'
        )
        if filename:
            with open(filename, 'w', encoding='utf-8') as fh:
                fh.write(self.textedit.toPlainText())

    def set_font(self):
        current = self.textedit.currentFont()
        accepted, font = qtw.QFontDialog.getFont(current, self)
        if accepted:
            self.textedit.setCurrentFont(font)

    def show_settings(self):
        settings_dialog = SettingsDialog(self.settings, self)
        settings_dialog.exec_()

if __name__ == '__main__':
    app = qtw.QApplication()
    main_window = MainWindow()
    sys.exit(app.exec_())
七月 22

Pyside2教程:modal和modeless

在pyside2中,Dialog box可以分为modal和modeless两种。

  • 如果为Modal,那么dialog boxes会组织用户与程序的其他部分交互。dialog boxes会暂停程序的执行,直到dialog boxes被关闭。另外,modal的dialog boxes执行完毕会返回一个值
  • 如果为Modeless,那么dialog boxes不会阻拦程序的执行,执行完毕后也没有返回值