七月 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不会阻拦程序的执行,执行完毕后也没有返回值
七月 22

Pyside2教程:日历小程序

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

class CategoryWindow(qtw.QWidget):
    submitted = qtc.Signal(str)

    def __init__(self):
        super().__init__()
        self.setLayout(qtw.QVBoxLayout())
        self.layout().addWidget(qtw.QLabel('请输入类型名称:'))
        self.category_entry = qtw.QLineEdit()
        self.layout().addWidget(self.category_entry)
        self.submit_btn = qtw.QPushButton('提交', clicked=self.onSubmit)
        self.layout().addWidget(self.submit_btn)
        self.cancel_btn = qtw.QPushButton('取消', clicked=self.close)
        self.layout().addWidget(self.cancel_btn)
        self.show()

    @qtc.Slot()
    def onSubmit(self):
        if self.category_entry.text():
            try:
                self.submitted.emit(self.category_entry.text())
            except AttributeError:
                pass
        self.close()

# 将主程序封装成类
class MainWindow(qtw.QWidget):
    # 注意这里应该是一个类变量而不是实例变量。只有这样,通过实例改变的数值才能全局性的改变。
    events = {}

    def __init__(self):
        # 引入父类的__init__方法
        super().__init__(parent=None)
        # Main UI code goes here
        self.setWindowTitle('日历')
        self.resize(800, 600)

        # 创建widgets
        self.calendar = qtw.QCalendarWidget()
        self.event_list = qtw.QListWidget()
        self.event_title = qtw.QLineEdit()
        self.event_category = qtw.QComboBox()
        self.event_time = qtw.QTimeEdit(qtc.QTime(8, 0))
        self.allday_check =qtw.QCheckBox('全天')
        self.event_detail =qtw.QTextEdit()
        self.add_button = qtw.QPushButton('新建/更新')
        self.del_button = qtw.QPushButton('删除')

        # 配置QComboBox
        self.event_category.addItems(['选择类型', '新建...', '工作', '会议', '就医', '家庭'])
        self.event_category.model().item(0).setEnabled(False)

        # 配置layout
        main_layout = qtw.QHBoxLayout()
        self.setLayout(main_layout)
        main_layout.addWidget(self.calendar)
        self.calendar.setSizePolicy(qtw.QSizePolicy.Expanding, qtw.QSizePolicy.Expanding)

        right_layout = qtw.QVBoxLayout()
        main_layout.addLayout(right_layout)
        right_layout.addWidget(qtw.QLabel('事件列表'))
        right_layout.addWidget(self.event_list)
        self.event_list.setSizePolicy(qtw.QSizePolicy.Expanding, qtw.QSizePolicy.Expanding)

        event_form = qtw.QGroupBox('事件管理')
        right_layout.addWidget(event_form)
        event_form_layout = qtw.QGridLayout()
        event_form.setLayout(event_form_layout)

        # 设置表单
        event_form_layout.addWidget(self.event_title, 1, 1, 1, 3)
        event_form_layout.addWidget(self.event_category, 2, 1)
        event_form_layout.addWidget(self.event_time, 2, 2)
        event_form_layout.addWidget(self.allday_check, 2, 3)
        event_form_layout.addWidget(self.event_detail, 3, 1, 1, 3)
        event_form_layout.addWidget(self.add_button, 4, 2)
        event_form_layout.addWidget(self.del_button, 4, 3)

        # 在选择全天时,disable时间选择控件
        self.allday_check.toggled.connect(self.event_time.setDisabled)
        # selectionChanged信号不传送数据
        self.calendar.selectionChanged.connect(self.populate_list)
        self.event_list.itemSelectionChanged.connect(self.populate_form)

        self.add_button.clicked.connect(self.save_event)
        self.del_button.clicked.connect(self.delete_event)

        self.event_list.itemSelectionChanged.connect(self.check_delete_btn)
        # 单独调用是为了保证删除按钮在启动时就处于禁用状态
        self.check_delete_btn()

        self.event_category.currentTextChanged.connect(self.on_category_change)

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

    # 工具方法,用于清除表单中的所有状态
    def clear_form(self):
        self.event_title.clear()
        self.event_category.setCurrentIndex(0)
        self.event_time.setTime(qtc.QTime(8, 0))
        self.allday_check.setChecked(False)
        self.event_detail.setPlainText('')

    def populate_list(self):
        self.event_list.clear()
        self.clear_form()
        date = self.calendar.selectedDate()
        # dict的get方法,第一个参数:键,第二个参数:如果指定键的值不存在时,返回该默认值
        for event in self.events.get(date, []):
            time = (event['time'].toString('hh:mm') if event['time'] else '全天')
            self.event_list.addItem("{}: {}".format(time, event['title']))

    def populate_form(self):
        self.clear_form()
        date = self.calendar.selectedDate()
        event_number = self.event_list.currentRow()
        if event_number == -1:
            return

        event_data = self.events.get(date)[event_number]
        self.event_category.setCurrentText(event_data['category'])
        if event_data['time'] is None:
            self.allday_check.setChecked(True)
        else:
            self.event_time.setTime(event_data['time'])

        self.event_title.setText(event_data['title'])
        self.event_detail.setPlainText(event_data['detail'])

    def save_event(self):
        event = {
            'category': self.event_category.currentText(),
            'time': (None if self.allday_check.isChecked() else self.event_time.time()),
            'title': self.event_title.text(),
            'detail': self.event_detail.toPlainText()
        }
        date = self.calendar.selectedDate()
        event_list = self.events.get(date, [])
        event_number = self.event_list.currentRow()

        if event_number == -1:
            event_list.append(event)
        else:
            event_list[event_number] = event

        event_list.sort(key=lambda x: x['time'] or qtc.QTime(0, 0))
        self.events[date] = event_list
        self.populate_list()

    def delete_event(self):
        date = self.calendar.selectedDate()
        row = self.event_list.currentRow()
        del(self.events[date][row])
        # 将currentRow设置成-1,表示没有选择
        self.event_list.setCurrentRow(-1)
        self.clear_form()
        self.populate_list()

    def check_delete_btn(self):
        self.del_button.setDisabled(self.event_list.currentRow() == -1)

    def add_category(self, category):
        self.event_category.addItem(category)
        self.event_category.setCurrentText(category)

    def on_category_change(self, text):
        if text == '新建...':
            self.dialog = CategoryWindow()
            self.dialog.submitted.connect(self.add_category)
            self.event_category.setCurrentIndex(0)

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

Pyside2教程:使用QtCore.Signal和QtCore.Slot案例

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

class FormWindow(qtw.QWidget):
    # 自定义信号,这个信号携带的是string类型的数据。注意str是一个type object
    # 在pyqt中,QtCore.Signal对应的函数为QtCore.pyqtSignal
    # submitted = qtc.Signal(str)
    # 用两个列表表示两种signature。一种只有一个str,另一种有int和str
    submitted = qtc.Signal([str], [int, str])

    def __init__(self):
        super().__init__()
        self.setLayout(qtw.QVBoxLayout())

        self.edit = qtw.QLineEdit()
        self.submit = qtw.QPushButton('提交', clicked=self.onSubmit)

        self.layout().addWidget(self.edit)
        self.layout().addWidget(self.submit)

    @qtc.Slot()
    def onSubmit(self):
        # 调用自定义submitted信号的emit方法来定义信号施放的数据
        if self.edit.text().isdigit():
            text = self.edit.text()
            # 注意这里在signal后面要用[]标记出对应的signature
            self.submitted[int, str].emit(int(text), text)
        else:
            # 注意这里在signal后面要用[]标记出对应的signature
            self.submitted[str].emit(self.edit.text())
        self.close()

# 将主程序封装成类
class MainWindow(qtw.QWidget):
    def __init__(self):
        # 引入父类的__init__方法
        super().__init__(parent=None)
        # Main UI code goes here
        self.setLayout(qtw.QVBoxLayout())

        self.label = qtw.QLabel('点击"变"来改变文字')
        self.change = qtw.QPushButton('变', clicked=self.onChange)

        self.layout().addWidget(self.label)
        self.layout().addWidget(self.change)

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

    @qtc.Slot()
    def onChange(self):
        self.formwindow = FormWindow()
        # 注意这里在signal后面要用[]标记出对应的signature
        self.formwindow.submitted[str].connect(self.onSubmittedStr)
        # 注意这里在signal后面要用[]标记出对应的signature
        self.formwindow.submitted[int, str].connect(self.onSubmittedIntStr)
        self.formwindow.show()

    @qtc.Slot(str)
    def onSubmittedStr(self, string):
        self.label.setText(string)

    @qtc.Slot(int, str)
    def onSubmittedIntStr(self, integer, string):
        text = 'The string {} becomes the number {}'.format(string, integer)
        self.label.setText(text)

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

Pyqt5中的pyqtSignal和pyqtSlot在Pyside2中叫什么

Pyqt5中的QtCore.pyqtSignal对应Pyside2中的QtCore.Signal
Pyqt5中的QtCore.pyqtSlot对应Pyside2中的QtCore.Slot

如果想在Pyqt5中使用Pyside2的命名规则:

QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot

QtCore.Signal用于生成一个信号。
QtCore.Slot用作装饰器。这个装饰器是完全可以省略的,因为任何可调用实例都可以用作槽。但是这个装饰器可以指定槽当中可以接收的数据类型,让槽的实现更加安全。

七月 18

Pyside2教程:信号与槽基础

名词解释

  • signal:实例的属性。它可以在某种事件发生时被施放
  • event:用户操作,定时器结束,异步方法完成等
  • slot:实例的方法。它可以接收信号,并相应的做出反应。注意此处的方法不一定是Qt的槽方法

我们将信号与槽连接在一起来配置我们的应用对事件的相应

# 信号与槽的用法
object1.singalName.connect(object2.slotName)

信号可携带槽可以接收的数据

# 这里的entry1和entry2都是QLineEdit
self.entry1.textChanged.connect(self.entry2.setText)

信号可以connect到其他的信号

# returnPress信号当按回车键时施放,editingFinished信号当按回车键或失去焦点时施放
# 这里需要用lambda表达式的原始connect()方法接收的是函数,而不是函数的返回值
self.entry1.editingFinished.connect(lambda : print('编辑完成'))
self.entry2.returnPressed.connect(self.entry1.editingFinished)

如果一个槽有多重不同signature的实现方式,这个槽叫做overloaded slot。PyQt会自动判断应该使用哪个槽的实现

  • 如果槽需要的参数比信号发出的参数多,那么Qt会报错
  • 反之,如信号发出的参数比槽需要的参数多,则Qt不会报错,只是丢弃多于的参数
七月 18

Pyside2教程:使用正则表达式进行输入校验

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

# 将主程序封装成类
class MainWindow(qtw.QWidget):
    def __init__(self):
        # 引入父类的__init__方法
        super().__init__(parent=None)
        # Main UI code goes here
        inventory_number = qtw.QLineEdit(self)
        entry_validator = qtg.QRegExpValidator(r'^[A-HJ-NP-Z]{2}-\d{3}-\d{4}[A-HJ-NP-Z]$')
        inventory_number.setValidator(entry_validator)
        # End main UI code
        # 显示主窗口
        self.show()

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

Pyside2教程:给QComboBox设置placeholder

event_category = qtw.QComboBox()
event_category.addItem(['选择类目', '新建', '工作', '会议', '就医', '家庭'])
# 将第一个选项设置为不可选择。这种用法类似于设定一个placeholder
self.event_category.model().item(0).setEnabled(False)
# 使用QComboBox类中的model()方法获取其中的数据(QStandardItemModel)
# 从QStandardItemModel实例中取数据需要用item()方法
# 将取出来的数据设置成不可选择
七月 16

Pyside2使用widget建立表单代码示例

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

class IPv4Validator(qtg.QValidator):
    """检查输入的是否是IPv4地址"""
    def validate(self, arg__1, arg__2):
        octet = arg__1.split('.')
        if len(octet) > 4:
            state = qtg.QValidator.Invalid
        elif not all(x.isdigit() for x in octet if x != ''):
            state = qtg.QValidator.Invalid
        elif not all(255 >= int(x) >= 0 for x in octet if x != ''):
            state = qtg.QValidator.Invalid
        elif len(octet) < 4:
            state = qtg.QValidator.Intermediate
        elif any(x == '' for x in octet):
            state = qtg.QValidator.Intermediate
        else:
            state = qtg.QValidator.Acceptable
        return state

class ChoiceSpinBox(qtw.QSpinBox):
    """带有文字选项的spin box"""
    def __init__(self, choices, *args, **kwargs):
        self.choices = choices
        super().__init__(*args, maximum=len(self.choices)-1, minimum=0, **kwargs)

    def valueFromText(self, text):
        return self.choices.index(text)

    def textFromValue(self, val):
        try:
            return self.choices[val]
        except IndexError:
            return '!Error!'

    def validate(self, input, pos):
        if input in self.choices:
            state = qtg.QValidator.Acceptable
        elif any(v.startswith(input) for v in self.choices):
            state = qtg.QValidator.Intermediate
        else:
            state = qtg.QValidator.Invalid
        return state

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

        # 创建和设置各种widget

        # 注意在构造QLabel时,第一个参数为显示的字符串,第二个参数才是parent
        label = qtw.QLabel('<b>我是一个QLabel!</b>', self, margin=10)

        # 单行的文字编辑框
        line_edit = qtw.QLineEdit('我是一个QLineEdit', self, placeholderText='在此输入')

        # 注意快捷键设置要使用QtGui中的QKeySequence类的对象。
        # 构造QKeySequence对象可以用字符串QKeySequence('ctrl+p'),也可用enum值QKeySequence(qtc.Qt.CTRL + qtc.Qt.Key_P)
        button = qtw.QPushButton('我是一个QPushButton', self, shortcut=qtg.QKeySequence('ctrl+p'))

        # 组合框不能在构造函数中初始化选项,需要在后面用addItem或者insertItem方法
        combobox = qtw.QComboBox(self, editable=True, insertPolicy=qtw.QComboBox.InsertAtTop)
        # addItem中的第一个参数为显示的标签,第二个参数为对应的数据值。这个数据值可以是数字,字符串或者python类
        # 当前选定的数据值可以用currentData方法获得
        combobox.addItem('Lemon', 1)
        combobox.addItem('Peach', 'Ohh I like Peaches!')
        combobox.addItem('Strawberry', qtw.QWidget)
        combobox.insertItem(1, 'Radish', 2)
        # 如果我们不需要给选项绑定对应的数据值,我们可以使用addItems和insertItems来一次添加多个选项

        # 注意QSpinBox中只有value对应的数字的部分是可以编辑的。其他的字符部分都是不可编辑的。输入的字符也必须是数字,但不必要考虑singleStep。
        # QDoubleSpinBox和QSpinBox类似,只是QDoubleSpinBox的数值为浮点数,而QSpinBox的数值为整数。
        spinbox = qtw.QSpinBox(self, value=12, maximum=100, minimum=10, prefix='$', suffix=' +Tax', singleStep=5)

        # 除了QDateTimeEdit,还有QDateEdit和QTimeEdit
        # QDate,QTime和QDateTime都属于QtCore。除此之外,也可以使用python标准的date,time和datetime
        datetimebox = qtw.QDateTimeEdit(self,
                                        date=qtc.QDate.currentDate(),
                                        time=qtc.QTime.currentTime(),
                                        calendarPopup=True,
                                        maximumDate=qtc.QDate(2030, 1, 1),
                                        maximumTime=qtc.QTime(17, 0),
                                        displayFormat='yyyy-MM-dd HH:mm')

        # 和QLineEdit相比,QTextEdit可以显示或者编辑多行文本,并且支持富文本。
        textedit = qtw.QTextEdit(self,
                                 acceptRichText=False,
                                 lineWrapMode=qtw.QTextEdit.FixedColumnWidth,
                                 lineWrapColumnOrWidth=25,
                                 placeholderText='请输入文字')

        # 创建和设置各种layout
        # 设置主layout
        layout = qtw.QVBoxLayout()
        self.setLayout(layout)
        layout.addWidget(label)
        layout.addWidget(line_edit)

        # 设置子layout
        sublayout = qtw.QHBoxLayout()
        layout.addLayout(sublayout)
        sublayout.addWidget(button)
        sublayout.addWidget(combobox)

        # 设置QGridLayout
        grid_layout = qtw.QGridLayout()
        # layout.addLayout(grid_layout)
        grid_layout.addWidget(spinbox, 0, 0)
        grid_layout.addWidget(datetimebox, 0, 1)
        grid_layout.addWidget(textedit, 1, 0, 2, 2)

        # 设置QFormLayout
        form_layout = qtw.QFormLayout()
        layout.addLayout(form_layout)
        form_layout.addRow('Item 1', qtw.QLineEdit(self))
        form_layout.addRow('Item 2', qtw.QLineEdit(self))
        form_layout.addRow(qtw.QLabel('<b>This is a label-only row</b>'))

        # 设置widget的尺寸
        label.setFixedSize(150, 40)
        line_edit.setMinimumSize(150, 15)
        line_edit.setMaximumSize(500, 50)
        spinbox.setSizePolicy(qtw.QSizePolicy.Fixed, qtw.QSizePolicy.Preferred)
        textedit.setSizePolicy(qtw.QSizePolicy.MinimumExpanding, qtw.QSizePolicy.MinimumExpanding)

        # 利用addWidget方法中的stretch参数设置
        stretch_layout = qtw.QHBoxLayout()
        layout.addLayout(stretch_layout)
        stretch_layout.addWidget(qtw.QLineEdit('short'), 1)
        stretch_layout.addWidget(qtw.QLineEdit('long'), 2)

        # 使用QTabWidget
        tab_widget = qtw.QTabWidget(movable=True, tabPosition=qtw.QTabWidget.West, tabShape=qtw.QTabWidget.Triangular)
        layout.addWidget(tab_widget)
        container = qtw.QWidget()
        container.setLayout(grid_layout)
        tab_widget.addTab(container, 'Tab the first')

        # 使用QGroupBox
        groupbox = qtw.QGroupBox('Buttons', checkable=True, checked=True, alignment=qtc.Qt.AlignHCenter, flat=True)
        groupbox.setLayout(qtw.QHBoxLayout())
        groupbox.layout().addWidget(qtw.QPushButton('Ok'))
        groupbox.layout().addWidget(qtw.QPushButton('Cancel'))
        layout.addWidget(groupbox)

        # 使用QValidator
        line_edit.setText('0.0.0.0')
        line_edit.setValidator(IPv4Validator())

        # 创建带字符串的spin box
        ratingbox = ChoiceSpinBox(('bad', 'average', 'good', 'awesome'), self)
        sublayout.addWidget(ratingbox)

        # End main UI code

        # 显示主窗口
        self.show()

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