PySide6 و ساخت فایل اجرایی
با دومین ابزار بزرگ رابط گرافیکی آشنا میشویم: همان چیزی که برنامههای دسکتاپ حرفهای واقعاً با آن نوشته میشوند. پنجره را با ماوس در Qt Designer طراحی میکنیم، و در پایان برنامه پایتون را به یک فایل تبدیل میکنیم که روی کامپیوتری بدون پایتون هم اجرا شود.
۱. Qt، و PySide6 یا PyQt6
Qt یک کتابخانه بزرگ ++C برای برنامههای دسکتاپ است و دو پل ارتباطی با پایتون دارد. هر دو تقریباً یکساناند و تفاوت اصلی در مجوز است.
| PySide6 | PyQt6 | |
|---|---|---|
| سازنده | شرکت Qt (رسمی) | Riverbank Computing |
| مجوز | LGPL، رایگان حتی برای برنامه تجاری بسته | GPL یا مجوز پولی |
| وارد کردن | from PySide6.QtWidgets import ... | from PyQt6.QtWidgets import ... |
| طراح | pyside6-designer | جداگانه نصب میشود |
| کامپایلر ui | pyside6-uic | pyuic6 |
| سیگنال شخصی | Signal و Slot | pyqtSignal و pyqtSlot |
ما به خاطر مجوز، PySide6 را انتخاب میکنیم. هر مثال
این فصل با عوض کردن خطوط import به PyQt6 تبدیل میشود.
python -m venv .venv .venv\Scripts\activate # Windows source .venv/bin/activate # Linux / macOS pip install -r requirements.txt
tkinter جزو خود پایتون است، نصب نمیخواهد و برای برنامههای کوچک عالی است. Qt باید نصب شود و حجیم است، ولی ویجتهای بسیار بیشتری دارد، ظاهرش حرفهایتر است، طراح گرافیکی دارد، و روی همه سیستمعاملها یکسان به نظر میرسد. برای یک ابزار کوچک tkinter، و برای یک محصول واقعی Qt.
۲. چهار قدم هر برنامه Qt
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
class HelloWindow(QWidget):
'''In Qt you make your own window by inheriting from a widget class.'''
def __init__(self) -> None:
super().__init__()
self.setWindowTitle('Kadoos - first Qt window')
self.resize(420, 220)
title = QLabel('Welcome to Kadoos Institute!')
# A stylesheet: Qt understands a subset of CSS.
title.setStyleSheet('font-size: 20px; font-weight: bold;')
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
subtitle = QLabel('This window is built with PySide6.')
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
# A layout places the widgets for you and follows the window when it
# is resized. Never position widgets by hand with move()/setGeometry().
layout = QVBoxLayout()
layout.addWidget(title)
layout.addWidget(subtitle)
self.setLayout(layout)
def main() -> None:
app = QApplication(sys.argv)
window = HelloWindow()
window.show()
sys.exit(app.exec())
اولین پنجره: نمونه 01_hello_window/app.py
شرح مثال
- هر برنامه Qt همین چهار قدم را دارد: ساختن
QApplication(دقیقاً یکی در هر برنامه)، ساختن ویجتها،show()، وapp.exec()که حلقه رویداد است، معادلmainloopدر tkinter. - ویجتها در Qt تا وقتی
show()صدا زده نشود پنهان هستند. این برخلاف tkinter است. - پنجره خودتان را با ارث بردن از
QWidgetمیسازید. کلاس، این بار اختیاری نیست بلکه روش استاندارد Qt است. - Qt زیرمجموعهای از CSS را میفهمد:
setStyleSheet. هر چه در دوره HTML و CSS یاد گرفتهاید اینجا به کار میآید. - در Qt هرگز ویجت را با مختصات پیکسلی نچینید. چیدمانها این کار را میکنند و با تغییر اندازه پنجره همراهی میکنند.
۳. سیگنال و اسلات
در tkinter مینوشتیم command=my_function. در Qt هر ویجت
سیگنال منتشر میکند و شما سیگنال را به یک تابع (که Qt
به آن اسلات میگوید) وصل میکنید.
این دقیقاً الگوی ناظر فصل سی و سوم است.
class CounterWindow(QWidget):
'''A counter and a live greeting.'''
def __init__(self) -> None:
super().__init__()
self.count = 0
self.count_label = QLabel('0')
plus = QPushButton('+')
minus = QPushButton('-')
reset = QPushButton('Reset')
# `lambda:` swallows the `checked` argument that clicked sends.
plus.clicked.connect(lambda: self.change(+1))
minus.clicked.connect(lambda: self.change(-1))
reset.clicked.connect(self.reset)
buttons = QHBoxLayout()
buttons.addWidget(minus)
buttons.addWidget(reset)
buttons.addWidget(plus)
self.name_edit = QLineEdit()
self.name_edit.setPlaceholderText('type your name...')
self.greeting = QLabel('Hello!')
# textChanged sends the new text to the slot.
self.name_edit.textChanged.connect(self.on_name_changed)
layout = QVBoxLayout()
layout.addWidget(self.count_label)
layout.addLayout(buttons) # a layout can hold another layout
layout.addWidget(self.name_edit)
layout.addWidget(self.greeting)
self.setLayout(layout)
def change(self, step: int) -> None:
self.count += step
self.count_label.setText(str(self.count))
def on_name_changed(self, text: str) -> None:
name = text.strip()
self.greeting.setText(f'Hello {name}!' if name else 'Hello!')
سیگنال و اسلات: نمونه 02_signals_and_slots/app.py
| سیگنال | کی منتشر میشود | چه میفرستد |
|---|---|---|
button.clicked | دکمه فشرده شد | یک بولی checked |
lineEdit.textChanged | متن عوض شد | متن تازه |
lineEdit.returnPressed | کلید Enter | چیزی نمیفرستد |
slider.valueChanged | مقدار عوض شد | عدد تازه |
comboBox.currentTextChanged | انتخاب عوض شد | متن انتخابشده |
شرح مثال
connectهم مثلcommandتابع را بدون پرانتز میگیرد.- یک سیگنال میتواند به چند اسلات وصل شود، و یک اسلات میتواند به چند سیگنال خدمت کند. این انعطاف، بزرگترین برتری Qt بر tkinter است.
- سیگنالی که مقدار حمل میکند، آن را به اسلات میدهد. برای همین
on_name_changedپارامترtextدارد. - دام ظریف:
clickedدر عمل یک بولیcheckedمیفرستد. اگر اسلات شما پارامتر ندارد، باlambdaآن را ببلعید یا خودتان پارامتر بگذارید. - یک چیدمان میتواند چیدمان دیگری را در خود بگیرد:
addLayout. اینطور ساختارهای پیچیده میسازید.
۴. ویجتها و چیدمانها
| ویجت | وظیفه آن | خواندن مقدار |
|---|---|---|
QLabel | متن یا تصویر | - |
QLineEdit | یک خط متن | .text() |
QTextEdit | چند خط متن | .toPlainText() |
QPushButton | دکمه | - |
QCheckBox | جعبه تیک | .isChecked() |
QComboBox | فهرست بازشو | .currentText() |
QSpinBox | عدد صحیح | .value() که واقعاً int است |
QListWidget | فهرست | .currentItem() |
| چیدمان | وظیفه آن |
|---|---|
QVBoxLayout | یکی زیر دیگری |
QHBoxLayout | یکی کنار دیگری |
QGridLayout | سطر و ستون، مثل grid در tkinter |
QFormLayout | جفت «برچسب: فیلد»، ابزار درست فرمها |
addStretch() | فضای خالی کشسان، برای هل دادن ویجتها |
class RegistrationForm(QWidget):
'''A course registration form built with QFormLayout.'''
def __init__(self) -> None:
super().__init__()
self.name_edit = QLineEdit()
self.course_box = QComboBox()
self.course_box.addItems(PRICES) # the keys of the dict
self.sessions_box = QSpinBox()
self.sessions_box.setRange(1, 20)
self.sessions_box.setValue(20)
self.student_box = QCheckBox('I am a student (20 % off)')
form = QFormLayout()
form.addRow('Full name:', self.name_edit)
form.addRow('Course:', self.course_box)
form.addRow('Sessions:', self.sessions_box)
form.addRow('', self.student_box)
buttons = QHBoxLayout()
buttons.addStretch() # push the buttons to the right
buttons.addWidget(clear)
buttons.addWidget(compute)
def compute(self) -> None:
name = self.name_edit.text().strip()
if not name:
self.result.setText('Please type your name first.')
return
course = self.course_box.currentText()
sessions = self.sessions_box.value() # already an int
price = PRICES[course] * sessions // 20
if self.student_box.isChecked():
price = int(price * 0.8)
self.result.setText(f'{name}: {course}, {sessions} sessions '
f'-> {price:,} toman')
فرم ثبتنام: نمونه 03_widgets_and_layouts/app.py
تفاوت ریز ولی مهمی را ببینید: QSpinBox.value() یک
int واقعی میدهد، برخلاف
Spinbox.get() در tkinter که رشته میداد. خود ویجت هم هر
چیزی جز عدد را نمیپذیرد. Qt در این موارد خیلی کارتان را راحتتر
میکند.
۵. دیالوگها و closeEvent
def ask_question(self) -> None:
answer = QMessageBox.question(self, 'Kadoos', 'Do you like Python?')
# Always compare against the StandardButton values.
liked = answer == QMessageBox.StandardButton.Yes
self.answer_label.setText('You answered: ' + ('yes' if liked else 'no'))
def open_file(self) -> None:
# getOpenFileName returns (path, filter) -- never forget the tuple.
path, _filter = QFileDialog.getOpenFileName(
self, 'Open', '', 'Text files (*.txt);;All files (*)')
if not path: # Cancel
return
self.answer_label.setText(f'You chose: {path}')
def closeEvent(self, event) -> None:
'''Qt calls this when the user clicks the X button.'''
answer = QMessageBox.question(self, 'Exit', 'Really close the program?')
if answer == QMessageBox.StandardButton.Yes:
event.accept() # let the window close
else:
event.ignore() # keep it open
دیالوگها: نمونه 04_message_boxes/app.py
شرح مثال
- چهار دیالوگ استاندارد:
information،warning،criticalوquestion. questionبرخلاف tkinter بولی برنمیگرداند، بلکه میگوید کدام دکمه فشرده شده است. پس همیشه باQMessageBox.StandardButton.Yesمقایسهاش کنید.QFileDialog.getOpenFileNameیک تاپل میدهد: مسیر و فیلتر انتخابشده. فراموش کردن تاپل، اولین اشتباه کلاسیک همه است. مسیر در صورت انصراف، رشته خالی است.closeEventمعادل Qt برایprotocol('WM_DELETE_WINDOW', ...)است. توجه کنید که این یک متد با نام از پیش تعیینشده است؛ Qt خودش صدایش میزند.event.accept()اجازه بستن میدهد وevent.ignore()جلویش را میگیرد.
۶. Qt Designer و فایل ui
Qt Designer برنامهای است که در آن ویجتها را با ماوس روی پنجره
میکشید و نتیجه را در یک فایل .ui ذخیره میکنید. آن فایل
XML ساده است و میتوانید در ویرایشگر متن بازش کنید و
بخوانید. این همان XML فصل بیست و هشتم است.
pyside6-designer
def load_ui(name: str) -> QWidget:
'''Load a .ui file that sits next to this script.'''
# `with_name` keeps the folder of THIS file, so the program works no
# matter which directory you run it from.
path = Path(__file__).with_name(name)
window = QUiLoader().load(str(path))
if window is None:
# A typo in the file name, or invalid XML.
raise RuntimeError(f'Cannot load {path}')
return window
def main() -> None:
app = QApplication(sys.argv)
window = load_ui('window.ui')
# findChild(type, objectName) -> the widget, or None if the name is wrong.
title = window.findChild(QLabel, 'labelTitle')
button = window.findChild(QPushButton, 'btnClose')
if title is None or button is None:
raise RuntimeError('An objectName does not match window.ui')
title.setText('Welcome to Kadoos Institute!')
button.clicked.connect(window.close)
window.show()
sys.exit(app.exec())
بارگذاری در زمان اجرا: نمونه 05_designer_first_ui/app.py
دو قانون طراحی در Designer
- به هر ویجتی که از پایتون به آن دست میزنید، یک objectName روشن بدهید:
labelTitle،btnClose،editName. همین نام است کهfindChildدنبالش جستوجو میکند. - ویجتها را حتماً در یک چیدمان بگذارید: راست کلیک روی فرم، سپس «Lay out». بدون چیدمان، ویجتها همان موقعیت پیکسلی را نگه میدارند و با تغییر اندازه پنجره یا با فونت متفاوت کاربر، ظاهر برنامه خراب میشود.
اگر نام را اشتباه بنویسید، findChild بدون هیچ خطایی
None برمیگرداند و برنامه خیلی بعدتر و دور از محل
اشتباه میترکد. همیشه بلافاصله بررسی کنید. نسخه اصلی این درس دقیقاً
همین بررسی را نداشت.
۷. ماشینحساب اصلاحشده
ماشینحساب اصلی این درس چهار مشکل داشت که هر چهار تا ارزش دیدن دارند:
چهار تابع هشتخطی که فقط در یک کاراکتر فرق داشتند،
float(edit.text()) بدون هیچ محافظی، تقسیم بر صفر، و
متغیری به نام sum که تابع درونی پایتون را پنهان میکرد.
import operator
from typing import Callable
# The four operations, as data instead of as four copies of the same function.
OPERATIONS: dict[str, Callable[[float, float], float]] = {
'btnAdd': operator.add,
'btnSub': operator.sub,
'btnMul': operator.mul,
'btnDiv': operator.truediv,
}
class Calculator:
'''A two-number calculator driven by calc.ui.'''
def __init__(self) -> None:
path = Path(__file__).with_name('calc.ui')
self.window: QWidget = QUiLoader().load(str(path))
...
for name, function in OPERATIONS.items():
button = self._find(QPushButton, name)
# `f=function` freezes the current value; without it every button
# would use the LAST operation of the loop.
button.clicked.connect(lambda checked=False, f=function:
self.calculate(f))
def read_numbers(self) -> tuple[float, float] | None:
'''Read both fields, or show why they cannot be read.'''
try:
first = float(self.edit1.text().strip())
second = float(self.edit2.text().strip())
except ValueError:
# Empty field, a letter, two dots... one message for all of them.
self.show_error('Please type two numbers.')
return None
return first, second
def calculate(self, function: Callable[[float, float], float]) -> None:
numbers = self.read_numbers()
if numbers is None:
return
try:
answer = function(*numbers)
except ZeroDivisionError:
self.show_error('Cannot divide by zero.')
return
self.result.setText(f'{answer:.2f}')
ماشینحساب: نمونه 07_designer_calculator/app.py
شرح مثال
- کلید حل مسئله، ماژول
operatorاست:operator.addهمان عملگر+پایتون است به شکل یک تابع معمولی. پس عملیات به یک مقدار تبدیل میشود که میشود در دیکشنری گذاشتش و پاسش داد. - چهار تابع تکراری به یک متد و یک دیکشنری تبدیل شدند. این همان درس الگوی کارخانه فصل سی و سوم است.
- باز هم اتصال دیرهنگام:
f=functionدر لامبدا. بدون آن، هر چهار دکمه آخرین عملیات حلقه را انجام میدهند. - در یک فایل
.exe، خطای مدیریتنشده یعنی «دکمه را میزنم و هیچ اتفاقی نمیافتد»، چون ردیابی خطا به کنسولی میرود که کاربر اصلاً نمیبیند. اعتبارسنجی در برنامه بستهبندیشده حیاتی است.
۸. کامپایل کردن فایل ui
راه دوم استفاده از فایل .ui این است که یک بار آن را به
کلاس پایتون کامپایل کنیم.
pyside6-uic window.ui -o ui_window.py # PyQt6: pyuic6
from PySide6.QtWidgets import QApplication, QWidget
from ui_window import Ui_Form
class TodoWindow(QWidget, Ui_Form):
'''A to-do list. The widgets come from the generated Ui_Form.'''
def __init__(self) -> None:
super().__init__()
self.setupUi(self) # builds every widget on `self`
self.btnAdd.clicked.connect(self.add_task)
self.editTask.returnPressed.connect(self.add_task)
self.btnRemove.clicked.connect(self.remove_task)
self.btnClear.clicked.connect(self.clear_tasks)
self.update_count()
def add_task(self) -> None:
text = self.editTask.text().strip()
if not text:
return
self.listTasks.addItem(text)
self.editTask.clear()
self.update_count()
def remove_task(self) -> None:
row = self.listTasks.currentRow() # -1 when nothing is selected
if row < 0:
return
# takeItem removes the row and gives the item back; we drop it.
self.listTasks.takeItem(row)
self.update_count()
نسخه کامپایلشده: نمونه 08_compile_ui_with_uic/app.py
| QUiLoader در زمان اجرا | pyside6-uic کامپایلشده |
|---|---|
| بعد از تغییر طرح، کاری ضرورتی ندارد | بعد از هر تغییر باید دوباره uic بزنید |
| ویجت با نام و در زمان اجرا پیدا میشود | ویجتها صفت واقعی شیء هستند |
| تکمیل خودکار ندارد، غلط املایی در زمان اجرا معلوم میشود | ویرایشگر کامل میکند و اشتباه را میگیرد |
| فایل ui باید همراه برنامه ارسال شود | فقط فایلهای py، پس بستهبندی سادهتر است |
اجرای بعدی pyside6-uic فایل
ui_window.py را کامل بازنویسی میکند و همه تغییرهای
دستیتان از بین میرود. کد خودتان در فایل جداگانه میماند. البته
یک بار داخل فایل تولیدشده را بخوانید: هیچ جادویی
در کار نیست، دقیقاً همان کدی است که در بخشهای اول این فصل دستی
نوشتیم.
۹. PyInstaller: اولین فایل اجرایی
PyInstaller پایتون را به کد ماشین کامپایل نمیکند. بلکه سه چیز را کنار هم بستهبندی میکند: فایلهای شما، خود مفسر پایتون، و همه کتابخانههایی که وارد کردهاید. نتیجه روی کامپیوتری که اصلاً پایتون ندارد اجرا میشود، و این تمام هدف ماجراست.
pip install pyinstaller pyinstaller --onefile app.py
| مسیر | چیست |
|---|---|
dist/app.exe | فایلی که به دیگران میدهید |
build/ | فایلهای موقت، قابل حذف |
app.spec | دستور پخت ساخت |
| گزینه | وظیفه آن |
|---|---|
--onefile | یک فایل واحد |
--onedir (پیشفرض) | یک پوشه شامل exe و کلی dll |
--name kadoos | نام خروجی |
--icon=app.ico | آیکون فایل |
--windowed | بدون پنجره سیاه کنسول |
--add-data | همراه بردن یک فایل داده |
--clean | دور ریختن حافظه نهان پیش از ساخت |
نکتههای عملی
- چون مفسر داخل فایل است، حجم خروجی بزرگ است: ده تا چهل مگابایت عادی است و با PySide6 به شصت تا نود مگابایت میرسد.
- خروجی فقط روی همان سیستمعاملی که ساخته شده اجرا میشود. ساخت روی ویندوز یعنی
.exeویندوزی. نمیتوانید با PyInstaller از لینوکس، خروجی ویندوزی بسازید. --onefileارسالش راحت است ولی هر بار خودش را در یک پوشه موقت باز میکند، پس کندتر راه میافتد. برای ابزار کوچک--onefile، برای برنامه بزرگ--onedir.- اگر آنتیویروس ویندوز شکایت کرد، معمولاً هشدار کاذب است: شکل خودبازشونده مشکوک به نظر میرسد و فایل امضا ندارد. ساخت با
--onedirمعمولاً حلش میکند.
۱۰. آیکون
pyinstaller --onefile --icon=app.ico app.py
آیکون ویندوز نه PNG است و نه JPEG. یک فایل .ico ظرفی است
که یک تصویر را در چند اندازه نگه میدارد، تا ویندوز
برای میزکار و نوار وظیفه و فهرست فایلها اندازه مناسب را بردارد.
from PIL import Image
image = Image.open('logo.png')
image.save('app.ico', sizes=[(16, 16), (32, 32), (48, 48), (256, 256)])
ساخت ico با Pillow، از فصل بیست و هشتم
دو نکته که همه را گیج میکند
- آیکون عوض نمیشود! تقصیر شما نیست: ویندوز آیکونها را در حافظه نهان نگه میدارد. فایل را تغییر نام بدهید یا به پوشه دیگری کپی کنید.
- آیکون پنجره چیز دیگری است. گزینه
--iconآیکون فایل را تعیین میکند. آیکونی که هنگام اجرا در نوار عنوان دیده میشود در کد تنظیم میشود:app.setWindowIcon(QIcon(...))و در tkinterroot.iconbitmap('app.ico').
۱۱. دامهای برنامه منجمد
pyinstaller --onefile --windowed --icon=app.ico --add-data "app.ico;." app.py
جداکننده در --add-data روی ویندوز
نقطهویرگول و روی لینوکس و مک دونقطه است. این تفاوت
کوچک، ساعتها وقت مردم را گرفته است.
وقتی یک برنامه --onefile اجرا میشود، خودش را در یک پوشه
موقت باز میکند و بعد از پایان پاکش میکند. پایتون آن
وقت اینها را میبیند:
| چیز | مقدارش در برنامه منجمد |
|---|---|
sys.frozen | True؛ این صفت در حالت عادی اصلاً وجود ندارد |
sys._MEIPASS | پوشه موقت |
__file__ | مسیری داخل همان پوشه موقت |
sys.executable | خود فایل exe، که جایش دائمی است |
'''The rule in one line:
resource_path() for what you READ and ship,
data_path() for what you WRITE.
'''
import sys
from pathlib import Path
def is_frozen() -> bool:
'''True when we are running inside a PyInstaller executable.'''
return getattr(sys, 'frozen', False)
def resource_path(name: str) -> Path:
'''Where to READ a file that was shipped with the program.'''
if is_frozen():
# PyInstaller unpacks --add-data files into sys._MEIPASS.
base = Path(sys._MEIPASS) # type: ignore[attr-defined]
else:
base = Path(__file__).parent
return base / name
def data_path(name: str) -> Path:
'''Where to WRITE a file so that it survives the next run.'''
if is_frozen():
# sys.executable is the .exe itself; its folder is permanent.
base = Path(sys.executable).parent
else:
base = Path(__file__).parent
return base / name
دو مسیر لازم هر برنامه منجمد: نمونه 11_pyinstaller_windowed/paths.py
چهار دام
-
با python اجرا میشود ولی exe فایلش را پیدا نمیکند.
Path(__file__).parent / 'logo.png'در حالت توسعه کار میکند و در exe شکست میخورد، چون فایل اصلاً داخل بسته کپی نشده است. با--add-dataبفرستیدش و باresource_path()پیدایش کنید. -
پایگاه دادهام هر بار خالی است. همان علت، اثری
بدتر: اگر کنار
__file__بنویسید، در پوشه موقت مینویسید و همه چیز با بسته شدن برنامه از بین میرود. برای نوشتن،data_path(). -
exe اصلاً هیچ کاری نمیکند. با
--windowedکنسولی وجود ندارد:printبه جایی نمیرود و خرابی کاملاً بیصداست. یک بار بدون--windowedبسازید و از ترمینال اجرا کنید تا ردیابی خطا را ببینید، یا از همان اول باloggingفصل نوزدهم یک فایل گزارش بنویسید. -
خروجی خیلی حجیم است. راهحلها:
--onedir، یک محیط مجازی تمیز تا فقط چیزهایی که واقعاً وارد کردهاید بستهبندی شوند، و--exclude-module.
۱۲. فایل spec
تایپ کردن دوباره و دوباره یک خط فرمان بلند، خستهکننده است. PyInstaller
یک دستور پخت مینویسد: فایل .spec، که
خودش کد پایتون ساده است و میتوانید بخوانید و
ویرایشش کنید.
# 1. write the recipe (without building anything) pyi-makespec --onefile --windowed --icon=app.ico app.py # 2. build from the recipe -- notice: the .spec, not the .py pyinstaller app.spec
a = Analysis(
['app.py'], # the entry point
pathex=[],
binaries=[],
datas=[('words.txt', '.')], # <- the data files: (source, destination)
hiddenimports=[], # <- imports PyInstaller could not find
excludes=['tkinter'], # <- modules to leave out, to shrink the build
...
)
exe = EXE(
...
name='kadoos', # the name of the result
console=False, # False = --windowed
icon='app.ico',
)
درون فایل spec: نمونه 12_pyinstaller_spec
| خط | برای چه |
|---|---|
datas | فرستادن تصویر، فایل ui، قالب پایگاه داده |
hiddenimports | ماژولی که فقط از راه importlib وارد میشود |
excludes | حذف چیزهایی که به کار نمیآیند، برای کوچک شدن خروجی |
وقتی از روی .spec میسازید، گزینههای خط فرمان مثل
--onefile نادیده گرفته میشوند؛
دستور پخت تصمیم میگیرد. فایل را ویرایش کنید، نه فرمان را.
دوم اینکه در datas از تاپل استفاده
میشود و آن ماجرای نقطهویرگول و دونقطه فقط مال خط فرمان است.
نکته آخر درباره وارد کردنهای پنهان: PyInstaller
وارد کردنها را با خواندن کد پیدا میکند. پس
importlib.import_module(name) برایش نامرئی است و برنامه
فقط داخل exe با ModuleNotFoundError
شکست میخورد. راه حل --hidden-import یا فهرست
hiddenimports است.
پوشه S36-pyside6-and-packaging شامل دوازده پوشه است.
نمونههای ۰۹ تا ۱۲ هر کدام README.md جداگانهای با
فرمانهای دقیق دارند. پیش از شروع، محیط مجازی بسازید و
pip install -r requirements.txt بزنید. Qt Designer را
با pyside6-designer باز کنید.
خلاصه فصل
| ابزار | وظیفه آن |
|---|---|
QApplication(sys.argv) | یکی در هر برنامه |
window.show() | ویجتها تا صدا نزنید پنهاناند |
app.exec() | حلقه رویداد Qt |
signal.connect(slot) | اتصال رویداد به تابع، بدون پرانتز |
QVBoxLayout و خانوادهاش | چیدمان. هرگز مختصات پیکسلی ندهید |
setStyleSheet | زیرمجموعهای از CSS |
QMessageBox.question | دکمه فشردهشده را برمیگرداند |
QFileDialog.getOpenFileName | یک تاپل برمیگرداند |
closeEvent | گرفتن دکمه بستن پنجره |
QUiLoader().load(path) | بارگذاری ui در زمان اجرا |
pyside6-uic | کامپایل ui به کلاس پایتون |
findChild(type, name) | ممکن است None بدهد. بررسی کنید |
pyinstaller --onefile | ساخت یک فایل اجرایی |
--windowed | حذف کنسول، برای هر برنامه گرافیکی |
--add-data | همراه بردن فایل داده |
sys.frozen و sys._MEIPASS | تشخیص و مکانیابی در برنامه منجمد |
resource_path و data_path | یکی برای خواندن، یکی برای نوشتن |
تمرینهای فصل سی و شش
- مبدل دمای فصل سی و چهارم را با PySide6 بازنویسی کنید.
- یک
QSliderبه نمونه دوم اضافه کنید وvalueChangedآن را هم به یک برچسب و هم به رنگ پسزمینه پنجره وصل کنید: یک سیگنال، دو اسلات. - به فرم ثبتنام، دو دکمه رادیویی داخل یک
QButtonGroupاضافه کنید و انتخاب را در نتیجه بیاورید. - فرم رمز عبور بسازید: دو
QLineEditبا حالت رمز، و برچسبی که زنده بگوید آیا دو رمز یکی هستند. - با
QMainWindowوQActionبه یکی از نمونهها نوار منو اضافه کنید. - پنجره ورود را در Designer بکشید و با
QUiLoaderبه وظیفه آن بیندازید. - همان فایل ui را با
pyside6-uicکامپایل کنید و به روش دوم راهش بیندازید. کدام را ترجیح دادید و چرا؟ - به ماشینحساب، دکمه درصد و دکمه جذر اضافه کنید، بدون نوشتن متد تازه برای هر کدام.
- ماشینحساب را به یک فایل اجرایی با آیکون و بدون کنسول تبدیل کنید. مواظب باشید فایل ui هم باید همراهش برود.
- برنامه مدیریت کتابخانه فصل سی و پنجم را به exe تبدیل کنید و مطمئن شوید
library.dbکنار فایل اجرایی نوشته میشود و دادهها بعد از بستن برنامه باقی میمانند. - عمداً یک ساخت را خراب کنید:
--add-dataرا بردارید، exe را اجرا کنید و پیام را بخوانید. بعد یک فایل گزارش اضافه کنید که همین خرابی را حتی در حالت پنجرهای ثبت کند. - حجم و زمان راهاندازی
--onefileو--onedirرا برای یک برنامه مقایسه کنید. - چرا PyInstaller نمیتواند روی لینوکس، فایل اجرایی ویندوز بسازد؟
- چرا برنامهای که پایگاه دادهاش را کنار
__file__مینویسد در حالت توسعه درست است و بعد از منجمد شدن غلط؟