使用PyQt編寫一個簡單的待辦程序
框架選擇
一個簡單的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() # 創(chuàng)建輸入和添加按鈕 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) # 創(chuàng)建操作按鈕 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
是網格布局,在添加子窗口時可以設定位置(行、列)和占據的大?。ㄕ紟仔袔琢校?/p>
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) # 創(chuàng)建 QGridLayout layout = QGridLayout(self) # 創(chuàng)建一個標題標簽 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) # 行, 列, 占據行數, 占據列數 # 創(chuàng)建一個 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開始 # 創(chuàng)建一個右側按鈕占據一列 right_button = QPushButton("Right Button", self) # 設置右側按鈕占據第3列的所有行 layout.addWidget(right_button, 1, 3, 3, 1) # 行1到3, 列3 # 創(chuàng)建一個底部按鈕占據一行 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
:
- 子窗口會嵌入到父窗口中,成為父窗口的一部分(類似按鈕、文本框等控件)。
- 子窗口的生命周期與父窗口綁定(父窗口銷毀時,子窗口自動銷毀)。
- 子窗口默認不會作為獨立窗口彈出,而是跟隨父窗口的布局顯示。
關鍵區(qū)別:
# 獨立窗口(無 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:希望子窗口作為模態(tài)對話框彈出
錯誤寫法:
parent_window = QWidget() child_window = QWidget(parent=parent_window) child_window.setWindowModality(Qt.ApplicationModal) # 設置為模態(tài) 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() # ? 作為模態(tài)對話框彈出
場景 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. 特殊場景:動態(tài)創(chuàng)建子窗口
若通過按鈕點擊動態(tài)創(chuàng)建子窗口,需確保子窗口的引用不被銷毀:
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) # 創(chuàng)建一個按鈕 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): """發(fā)射自定義信號""" 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 } ] }
大致就是新開一個窗口,創(chuàng)建一個QTextEdit
輸入對應的json數據
,然后通過json.loads()
方法解析對應數據,
逐個使用add_item()
接口添加,這要求add_item()
能夠處理多種情況:按鈕點擊觸發(fā)無需參數
和批量導入中需要傳遞參數
。
而在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 優(yōu)化時間顯示居右 current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 創(chuàng)建 ToDoItem 實例 todo_item = ToDoItem(title, description, current_time) # 創(chuàng)建新的列表項 創(chuàng)建自定義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) # 創(chuàng)建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解析
使用按鈕連接點擊信號至槽函數,發(fā)現槽函數add_item接收到的title參數不是預期的輸入值None(因為點擊事件的槽函數一般不帶參數),而是False。
1.PyQt 的 clicked
信號默認會傳遞一個布爾值:
QPushButton
的clicked
信號默認會發(fā)送一個checked
參數(表示按鈕的選中狀態(tài))。- 如果你沒有顯式處理這個參數,它會傳遞到槽函數中,導致
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
信號默認發(fā)送checked
(布爾值),而QPushButton
默認不可選中,因此總是發(fā)送False
。- 如果槽函數參數數量不匹配,第一個參數會接收這個
False
。
參數優(yōu)先級問題:
- 如果調用
add_item
時傳遞了參數(如add_item(title="測試")
),title
會被正確賦值。 - 若未傳遞參數,
title
會被錯誤地賦值為False
(來自checked
參數)。
導出
既然有批量導入
功能,就有導出功能
剪切板 QClipboard
在 PyQt 中,可以使用 QApplication.clipboard()
來訪問系統(tǒng)剪貼板,并通過 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")
自定義排序規(guī)則
在Python中自定義排序規(guī)則,你可以使用內置的sorted()函數或者列表對象的sort()方法,并通過key參數指定一個函數來定義排序規(guī)則。這個函數會對每個元素進行處理,并返回一個值,排序將根據這個返回值進行。
按字符串長度排序:
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']
復雜排序規(guī)則,先按字符串長度排序,再按字母順序排序:
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將傳統(tǒng)比較函數轉換為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]
優(yōu)化條目顯示
添加一個標題布局,顯示列表的標題 => 放一個水平布局在QListWidget上對齊就可以
分離顯示與數據(QlistWidget
):
- 不再直接使用 QListWidgetItem(text),而是通過
setItemWidget
綁定自定義Widget - 數據仍存儲在 ToDoItem 對象中,界面僅負責展示
自定義Widget布局控制:
- 使用 QHBoxLayout 實現水平分列
- setAlignment 控制對齊方向
- setContentsMargins 調整內容間距
Bug解析
文字出現了上下截斷的情況,嘗試過設置延伸策略,給item設置固定寬高都不能根治
發(fā)現隨著高度的變大,顯示的內容越來越多,所以猜測是QLabel有默認的padding
,所以截斷了文字
最后設置QStyleSheet
成功解決
# TODO 優(yōu)化時間顯示居右 # 創(chuàng)建 ToDoItem 實例 todo_item = ToDoItem(title, description,deadline_time,is_completed) # 創(chuàng)建新的列表項 創(chuàng)建自定義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) # 創(chuàng)建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待辦程序的資料請關注腳本之家其它相關文章!
相關文章
解決python3在anaconda下安裝caffe失敗的問題
下面小編就為大家?guī)硪黄鉀Qpython3在anaconda下安裝caffe失敗的問題。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06Python通過keyboard庫實現模擬和監(jiān)聽鍵盤
這篇文章主要為大家詳細介紹了Python如何通過keyboard庫實現模擬和監(jiān)聽鍵盤,文中的示例代碼講解詳細,感興趣的小伙伴可以了解下2024-10-10