框架选择
一个简单的gui程序,可以使用pyqt完成。pyqt是qt的python实现版本。
界面搭建
设计一个美观
简洁的界面
class todoapp(qwidget): def __init__(self): super().__init__() # 设置窗口属性 self.setwindowtitle("daily to do list") self.setgeometry(100, 100, 400, 400) # 初始化主布局 self.main_layout = qvboxlayout() # 创建输入和添加按钮 self.input_layout = qgridlayout() # 标题输入 self.title_label = qlabel("标题:") self.title_input = qlineedit() self.input_layout.addwidget(self.title_label, 0, 0) self.input_layout.addwidget(self.title_input, 0, 1) # 描述输入 self.description_label = qlabel("描述:") self.description_input = qlineedit() self.input_layout.addwidget(self.description_label, 1, 0) self.input_layout.addwidget(self.description_input, 1, 1) # 水平按钮布局 self.add_layout = qhboxlayout() self.add_layout.setspacing(20) # 导入 self.import_button = qpushbutton('批量导入') self.import_button.clicked.connect(self.import_item) self.add_layout.addwidget(self.import_button) self.add_button = qpushbutton("添加") self.add_button.clicked.connect(self.add_item) self.add_layout.addwidget(self.add_button) # 任务列表 self.to_do_list = qlistwidget() self.to_do_list.setstylesheet("padding: 10px;") self.to_do_list.setverticalscrollmode(qlistwidget.scrollperpixel) # 创建操作按钮 self.buttons_layout = qhboxlayout() self.mark_done_button = qpushbutton("标记完成") self.mark_done_button.clicked.connect(self.mark_item_done) self.delete_button = qpushbutton("删除") self.delete_button.clicked.connect(self.delete_item) self.buttons_layout.addwidget(self.mark_done_button) self.buttons_layout.addwidget(self.delete_button) # 将布局添加到主窗口 self.main_layout.addlayout(self.input_layout) self.main_layout.addlayout(self.add_layout) self.main_layout.addwidget(self.to_do_list) self.main_layout.addlayout(self.buttons_layout) # 设置窗口布局 self.setlayout(self.main_layout)
qgridlayout的使用
qgridlayout
是网格布局,在添加子窗口时可以设定位置(行、列)和占据的大小(占几行几列)
import sys from pyqt5.qtwidgets import qapplication, qwidget, qgridlayout, qlabel, qpushbutton class gridexample(qwidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setwindowtitle("pyqt qgridlayout example") self.setgeometry(100, 100, 400, 300) # 创建 qgridlayout layout = qgridlayout(self) # 创建一个标题标签 title_label = qlabel("header label", self) title_label.setstylesheet("font-size: 18px; font-weight: bold; color: blue;") # 将标题设置为占据第一行的所有列 layout.addwidget(title_label, 0, 0, 1, 3) # 行, 列, 占据行数, 占据列数 # 创建一个 3x3 的按钮网格 for i in range(3): for j in range(3): button = qpushbutton(f"button {i*3 + j +1}", self) layout.addwidget(button, i + 1, j) # 按钮从行1开始 # 创建一个右侧按钮占据一列 right_button = qpushbutton("right button", self) # 设置右侧按钮占据第3列的所有行 layout.addwidget(right_button, 1, 3, 3, 1) # 行1到3, 列3 # 创建一个底部按钮占据一行 bottom_button = qpushbutton("bottom button", self) # 设置底部按钮占据第4行的所有列 layout.addwidget(bottom_button, 4, 0, 1, 4) # 行4, 列0-3 self.setlayout(layout) if __name__ == "__main__": app = qapplication(sys.argv) window = gridexample() window.show() sys.exit(app.exec_())
强制子窗口独立
在 pyqt 中,当给窗口(qwidget
或子类)设置 parent
后窗口不显示,通常是因为 子窗口被嵌入到了父窗口的布局中,而非作为独立窗口显示。以下是常见原因和解决方案:
1. 根本原因:parent 的作用
parent 的作用:在 pyqt 中,parent
表示窗口的父控件。若设置 parent
:
- 子窗口会嵌入到父窗口中,成为父窗口的一部分(类似按钮、文本框等控件)。
- 子窗口的生命周期与父窗口绑定(父窗口销毁时,子窗口自动销毁)。
- 子窗口默认不会作为独立窗口弹出,而是跟随父窗口的布局显示。
关键区别:
# 独立窗口(无 parent) child_window = qwidget() child_window.show() # 嵌入父窗口(设置 parent) child_window = qwidget(parent=main_window) # 不会独立显示,而是嵌入到 main_window 中
2. 常见场景和解决方法
场景 1:希望子窗口作为独立窗口弹出
错误写法:
parent_window = qwidget() child_window = qwidget(parent=parent_window) # 设置 parent child_window.show() # ❌ 不会显示独立窗口!
原因:child_window
已成为 parent_window
的子控件,必须通过父窗口的布局显示(例如将 child_window
添加到父窗口的 qvboxlayout
中)。
解决方法:不要设置 parent,让子窗口独立:
parent_window = qwidget() child_window = qwidget() # 无 parent child_window.show() # ✅ 作为独立窗口显示
场景 2:希望子窗口作为模态对话框弹出
错误写法:
parent_window = qwidget() child_window = qwidget(parent=parent_window) child_window.setwindowmodality(qt.applicationmodal) # 设置为模态 child_window.show() # ❌ 仍然不显示!
原因:child_window
是 parent_window
的子控件,必须通过父窗口布局显示,或明确设置为独立窗口。
解决方法:使用 qt.window
标志强制子窗口成为独立窗口:
child_window = qwidget(parent=parent_window) child_window.setwindowflags(qt.window) # 关键:强制为独立窗口 child_window.setwindowmodality(qt.applicationmodal) child_window.show() # ✅ 作为模态对话框弹出
场景 3:子窗口被正确添加到父窗口布局但仍不显示
错误写法:
parent_window = qwidget() child_window = qwidget(parent=parent_window) parent_window.show() # ❌ 只显示 parent_window,但 child_window 未添加到布局中
原因:child_window
需要被添加到父窗口的布局管理器(如 qvboxlayout
),或手动设置其位置。
解决方法:将子窗口添加到父窗口布局:
parent_window = qwidget() layout = qvboxlayout(parent_window) # 父窗口设置布局 child_window = qwidget() layout.addwidget(child_window) # 添加到布局 parent_window.show() # ✅ 父窗口和子控件均显示
3. 通用检查列表
如果子窗口不显示,按以下步骤排查:
.是否设置 parent:
若设置 parent
,子窗口需要添加到父窗口的布局中。
.父窗口是否已显示:
父窗口调用 show()
后,子控件才会显示。
.窗口标志是否正确:
使用 setwindowflags(qt.window)
强制子窗口独立。
.布局是否正确:
确保子窗口被添加到父窗口的布局管理器(如 addwidget(child)
)。
.生命周期问题:
确保父窗口未被提前销毁(例如在局部作用域中被垃圾回收)。
4. 完整示例对比
示例 1:子窗口嵌入父窗口(正确写法)
from pyqt5.qtwidgets import qwidget, qvboxlayout, qpushbutton, qapplication app = qapplication([]) # 父窗口 parent = qwidget() layout = qvboxlayout(parent) # 子窗口(作为控件嵌入父窗口) child = qpushbutton("我是子控件", parent=parent) layout.addwidget(child) parent.show() app.exec_()
示例 2:子窗口作为独立窗口(正确写法)
from pyqt5.qtwidgets import qwidget, qapplication app = qapplication([]) # 父窗口 parent = qwidget() parent.show() # 子窗口(独立窗口,无 parent) child = qwidget() child.setwindowtitle("我是独立子窗口") child.show() app.exec_()
5. 特殊场景:动态创建子窗口
若通过按钮点击动态创建子窗口,需确保子窗口的引用不被销毁:
from pyqt5.qtwidgets import qwidget, qpushbutton, qvboxlayout, qapplication app = qapplication([]) class mainwindow(qwidget): def __init__(self): super().__init__() self.button = qpushbutton("打开子窗口", self) self.button.clicked.connect(self.open_child) self.setlayout(qvboxlayout()) self.layout().addwidget(self.button) def open_child(self): self.child = qwidget() # 必须保存为成员变量,否则会被垃圾回收! self.child.setwindowtitle("子窗口") self.child.show() window = mainwindow() window.show() app.exec_()
通过理解 parent
的作用和布局机制,可以灵活控制窗口的显示方式。
模型设计
每一个待办事项有
- 标题
- 描述信息
- 生成时间
- 是否完成标识
- 截止时间
class todoitem: def __init__(self, title, description,deadline_time=none,is_completed=false): self.title = title self.description = description self.deadline_time = deadline_time self.is_completed = false
自定义信号
from pyqt5.qtwidgets import qapplication, qwidget, qpushbutton, qvboxlayout, qmessagebox from pyqt5.qtcore import pyqtsignal import sys class mywidget(qwidget): # 定义一个自定义信号 custom_signal = pyqtsignal(str) def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setwindowtitle("pyqt custom signal example") self.setgeometry(100, 100, 300, 200) # 创建一个按钮 self.button = qpushbutton("click me", self) self.button.clicked.connect(self.emit_custom_signal) # 设置布局 layout = qvboxlayout() layout.addwidget(self.button) self.setlayout(layout) # 连接自定义信号到槽 self.custom_signal.connect(self.handle_custom_signal) def emit_custom_signal(self): """发射自定义信号""" self.custom_signal.emit("hello from custom signal!") def handle_custom_signal(self, message): """处理自定义信号""" qmessagebox.information(self, "custom signal", message) if __name__ == "__main__": app = qapplication(sys.argv) widget = mywidget() widget.show() sys.exit(app.exec_())
批量导入功能
使用json
格式的字符串进行批量导入,大致格式如下
{ "items": [ { "title": "测试标题1", "description": "测试1", "deadline_time": "2024", "is_completed": false }, { "title": "测试标题2", "description": "测试2", "deadline_time": "2025", "is_completed": true } ] }
大致就是新开一个窗口,创建一个qtextedit
输入对应的json数据
,然后通过json.loads()
方法解析对应数据,
逐个使用add_item()
接口添加,这要求add_item()
能够处理多种情况:按钮点击触发无需参数
和批量导入中需要传递参数
。
而在python中实现类似重载的效果可以给参数一个默认值none
,再在函数内部分情况处理
class importwidget(qwidget): # 自定义信号 import_finished = pyqtsignal(dict) def __init__(self,parent): super().__init__(parent) self.setfixedsize = (500,500) self.setwindowflags(qt.window) # 关键:强制为独立窗口 self.input_field = qtextedit() self.main_layout = qvboxlayout() self.btn_layout = qhboxlayout() self.confirm_button = qpushbutton("导入") self.cancel_button = qpushbutton("取消") self.btn_layout.addwidget(self.confirm_button) self.btn_layout.addwidget(self.cancel_button) self.confirm_button.clicked.connect(self.read_json_data) # 连接自定义信号到槽 self.import_finished.connect(self.close) self.main_layout.addlayout(self.btn_layout) self.main_layout.addwidget(self.input_field) self.setlayout(self.main_layout) def read_json_data(self): text = self.input_field.toplaintext() # 获取输入框的文本 # print(f"原始文本内容: {text}") # 调试:打印原始文本内容 try: # 将输入的文本解析为 json 数据 json_data = json.loads(text.strip()) # 使用 strip() 去除首尾空白字符 # print(f"解析后的 json 数据: {json_data}") self.json_data = json_data self.import_finished.emit(json_data) except json.jsondecodeerror as e: # 如果 json 格式不正确,打印错误信息 print(f"json 解析失败: {e}") self.json_data = none
class todoapp(qwidget): def batch_import(self,json_data): print(json_data['items']) items = json_data['items'] for item in items: self.add_item(item['title'],item['description']) def add_item(self, checked,title=none, description=none): # 如果 title 和 description 是传入的参数 if title is not none or description is not none: # 使用传入的参数 title = title.strip() if title else "" description = description.strip() if description else "" else: # 获取输入框的文本 title = self.title_input.text().strip() print(title, description) description = self.description_input.text().strip() # todo 优化时间显示居右 current_time = datetime.datetime.now().strftime("%y-%m-%d %h:%m:%s") # 创建 todoitem 实例 todo_item = todoitem(title, description, current_time) # 创建新的列表项 创建自定义widget item_widget = qwidget() layout = qhboxlayout() # 标题部分 title_label = qlabel(todo_item.title) title_label.setstylesheet("qlabel{padding:0px}") title_label.setalignment(qt.alignmentflag.alignleft| qt.alignmentflag.alignvcenter) # 时间部分 time_label = qlabel(todo_item.created_time) time_label.setstylesheet("qlabel{padding:0px}") # 添加padding设置,qlabel有默认padding,不设置话,会将文字截断 time_label.setalignment(qt.alignmentflag.alignright | qt.alignmentflag.alignvcenter) # 添加控件到布局 layout.addwidget(title_label) layout.addwidget(time_label) item_widget.setlayout(layout) # 创建listwidgetitem item = qlistwidgetitem() item.setsizehint(item_widget.sizehint()) # 设置每一项的宽高 item.settooltip(todo_item.description) # 设置悬浮提示 item.setdata(qt.userrole, todo_item) # 保存任务对象 item.setflags(item.flags() | qt.itemisselectable | qt.itemisenabled) item.setcheckstate(qt.checkstate.unchecked) self.to_do_list.additem(item) self.to_do_list.setitemwidget(item, item_widget) # 清空输入框 self.title_input.clear() self.description_input.clear()
bug解析
使用按钮连接点击信号至槽函数,发现槽函数add_item接收到的title参数不是预期的输入值none(因为点击事件的槽函数一般不带参数),而是false。
1.pyqt 的 clicked
信号默认会传递一个布尔值:
qpushbutton
的clicked
信号默认会发送一个checked
参数(表示按钮的选中状态)。- 如果你没有显式处理这个参数,它会传递到槽函数中,导致
title
参数被赋值为false
(因为默认未选中)。
2.槽函数定义与信号参数不匹配:
你定义的 add_item
方法有两个可选参数:
def add_item(self, title=none, description=none):
当通过 self.add_button.clicked.connect(self.add_item)
连接信号时,clicked
信号的 checked
参数(布尔值)会传递给 title
参数。
因此,点击按钮时 title
实际接收到的是 false
,而不是预期的 none
。
方法 1:显式接收并忽略 checked
参数
修改槽函数,增加一个参数接收 checked
值,但不在内部使用它:
def add_item(self, checked, title=none, description=none): # 增加 checked 参数 # 如果 title 和 description 是传入的参数 if title is not none or description is not none: print("not null", title, description) title = title.strip() if title else "" description = description.strip() if description else "" else: # 获取输入框的文本 title = self.title_input.text().strip() description = self.description_input.text().strip() # 其他逻辑...
方法 2:使用 lambda
阻止参数传递
在连接信号时,通过 lambda
屏蔽 clicked
信号的参数:
self.add_button.clicked.connect(lambda: self.add_item()) # 不传递任何参数
此时 title
和 description
将保持 none
,代码会从输入框中读取值。
关键点解释
信号参数传递机制:
clicked
信号默认发送checked
(布尔值),而qpushbutton
默认不可选中,因此总是发送false
。- 如果槽函数参数数量不匹配,第一个参数会接收这个
false
。
参数优先级问题:
- 如果调用
add_item
时传递了参数(如add_item(title="测试")
),title
会被正确赋值。 - 若未传递参数,
title
会被错误地赋值为false
(来自checked
参数)。
导出
既然有批量导入
功能,就有导出功能
剪切板 qclipboard
在 pyqt 中,可以使用 qapplication.clipboard()
来访问系统剪贴板,并通过 qclipboard
类的方法将数据复制到剪贴板
def export_to_clipboard(self): # 获取所有任务 items = [] for i in range(self.to_do_list.count()): item = self.to_do_list.item(i) if item: todo_item = item.data(qt.userrole) items.append({ "title": todo_item.title, "description": todo_item.description, "deadline_time": todo_item.deadline_time, "is_completed": todo_item.is_completed }) # 转换为 json 格式 json_data = { "items": items } json_str = json.dumps(json_data, indent=4, ensure_ascii=false) # 格式化 json 字符串 # 复制到剪切板 clipboard = qapplication.clipboard() clipboard.settext(json_str) # 弹出提示 qmessagebox.information(self, "提示", "已复制到剪切板")
常用的 qclipboard
方法
settext(text)
: 将文本复制到剪贴板。setpixmap(pixmap)
: 将图片复制到剪贴板。setmimedata(mimedata)
: 将 mime 数据(如 html)复制到剪贴板。clear()
: 清除剪贴板内容。
持久化存储
- 数据库sqlite
- 文件保存
直接写入文件,不使用数据库了,重写关闭事件
,保存代办到文件,并在初始化的时候读取文件
def init_from_file(self, file_path=none): # 默认初始化文件为当前目录下的 to_do.json if file_path is none: file_path = "./to_do.json" # 读取文件内容 with open(file_path, "r", encoding="utf-8") as f: text = f.read() self.batch_import(json.loads(text)) def closeevent(self, event): # 关闭窗口时保存数据 with open("./to_do.json", "w", encoding="utf-8") as f: self.export_to_clipboard(true) f.write(qapplication.clipboard().text()) qapplication.clipboard().clear() event.accept()
排序功能
截止时间ddl排序
def sort_by_ddl(self): if self.sort_value == "asc": self.sort_value = "desc" else: self.sort_value = "asc" # 按 ddl 排序 items = [] for i in range(self.to_do_list.count()): item = self.to_do_list.item(i) if item: todo_item = item.data(qt.userrole) items.append(todo_item) # 根据self.sort_value决定排序方向 if self.sort_value == "asc": items.sort(key=self.sort_key) else: items.sort(key=self.sort_key, reverse=true) # 清空列表 self.to_do_list.clear() # 重新添加排序后的任务 for item in items: self.add_item(item.title, item.description, item.deadline_time, item.is_completed) def sort_key(self, item): item.deadline_time.replace(":",":") if item.deadline_time == "未知": return datetime.datetime.max else: return datetime.datetime.strptime(item.deadline_time.replace(":",":"), "%y-%m-%d %h:%m")
自定义排序规则
在python中自定义排序规则,你可以使用内置的sorted()函数或者列表对象的sort()方法,并通过key参数指定一个函数来定义排序规则。这个函数会对每个元素进行处理,并返回一个值,排序将根据这个返回值进行。
按字符串长度排序:
strings = ["apple", "banana", "cherry", "date"] sorted_strings = sorted(strings, key=len) print(sorted_strings) # 输出: ['date', 'apple', 'banana', 'cherry']
使用lambda函数按字符串的最后一个字符排序:
strings = ["apple", "banana", "cherry", "date"] sorted_strings = sorted(strings, key=lambda x: x[-1]) print(sorted_strings) # 输出: ['banana', 'apple', 'date', 'cherry']
复杂排序规则,先按字符串长度排序,再按字母顺序排序:
strings = ["apple", "banana", "cherry", "date"] sorted_strings = sorted(strings, key=lambda x: (len(x), x)) print(sorted_strings) # 输出: ['date', 'apple', 'banana', 'cherry']
使用cmp_to_key将传统比较函数转换为key函数:
from functools import cmp_to_key def compare(x, y): if x < y: return -1 elif x > y: return 1 else: return 0 numbers = [3, 2, 5, 4, 1] sorted_numbers = sorted(numbers, key=cmp_to_key(compare)) print(sorted_numbers) # 输出: [1, 2, 3, 4, 5]
优化条目显示
添加一个标题布局,显示列表的标题 => 放一个水平布局在qlistwidget上对齐就可以
分离显示与数据(qlistwidget
):
- 不再直接使用 qlistwidgetitem(text),而是通过
setitemwidget
绑定自定义widget - 数据仍存储在 todoitem 对象中,界面仅负责展示
自定义widget布局控制:
- 使用 qhboxlayout 实现水平分列
- setalignment 控制对齐方向
- setcontentsmargins 调整内容间距
bug解析
文字出现了上下截断的情况,尝试过设置延伸策略,给item设置固定宽高都不能根治
发现随着高度的变大,显示的内容越来越多,所以猜测是qlabel有默认的padding
,所以截断了文字
最后设置qstylesheet
成功解决
# todo 优化时间显示居右 # 创建 todoitem 实例 todo_item = todoitem(title, description,deadline_time,is_completed) # 创建新的列表项 创建自定义widget item_widget = qwidget() layout = qhboxlayout() # 标题部分 title_label = qlabel(todo_item.title) title_label.setstylesheet("qlabel{padding:0px}") title_label.setalignment(qt.alignmentflag.alignleft| qt.alignmentflag.alignvcenter) # 时间部分 time_label = qlabel(todo_item.deadline_time) time_label.setstylesheet("qlabel{padding:0px}") # 添加padding设置,qlabel有默认padding,不设置话,会将文字截断 time_label.setalignment(qt.alignmentflag.alignright | qt.alignmentflag.alignvcenter) # 添加控件到布局 layout.addwidget(title_label) layout.addwidget(time_label) item_widget.setlayout(layout) # 创建listwidgetitem item = qlistwidgetitem() item.setsizehint(item_widget.sizehint()) # 设置每一项的宽高 item.settooltip(todo_item.description) # 设置悬浮提示 item.setdata(qt.userrole, todo_item) # 保存任务对象 item.setflags(item.flags() | qt.itemisselectable | qt.itemisenabled |qt.itemisusercheckable) if todo_item.is_completed: item.setcheckstate(qt.checkstate.checked) else: item.setcheckstate(qt.checkstate.unchecked) self.to_do_list.additem(item) self.to_do_list.setitemwidget(item, item_widget) # 清空输入框 self.title_input.clear() self.description_input.clear() self.deadline_input.clear()
绑定自定义widget后,点击无法改变item
的checkstate
解决方法有很多种,这里采用连接父listwidget的双击信号
self.to_do_list.doubleclicked.connect(self.on_double_clicked) def on_double_clicked(self, index: qmodelindex): print(index.row()) # 打印行号 print(index.column()) # 打印列号(通常为 0) item = self.to_do_list.itemfromindex(index) # 获取 qlistwidgetitem if item.checkstate() == qt.checkstate.unchecked: item.setcheckstate(qt.checkstate.checked) elif item.checkstate() == qt.checkstate.checked : item.setcheckstate(qt.checkstate.unchecked) print(item.text()) # 打印项的文本
最终代码
https://github.com/0zxm/todoapp/tree/master
以上就是使用pyqt编写一个简单的待办程序的详细内容,更多关于pyqt待办程序的资料请关注代码网其它相关文章!
发表评论