۶. خواندن فرم، run و توابع دکمه‌ها

خط ۷۷ تا ۱۱۱

این بخش پل بین پنجره و کلاس Shop است. values متن جعبه‌ها را به عدد تبدیل می‌کند، run هر کاری را امن اجرا می‌کند و خطاها را نشان می‌دهد، و شش تابع کوتاه هر کدام کار یک دکمه را انجام می‌دهند.

کد این بخش

shop_manager.py, lines 77–111
77def values():78    name = fields['name'].get().strip()79    buy = float(fields['buy'].get())80    sell = float(fields['sell'].get())81    qty = int(fields['qty'].get())82    return name, buy, sell, qty83 84def run(action):85    try:86        action()87        if action is not search: refresh()88    except (ValueError, TypeError) as exc:89        messagebox.showerror('خطا', str(exc))90    except OSError as exc:91        messagebox.showerror('خطای فایل', str(exc))92 93def add():94    shop.add(*values())95 96def edit():97    shop.edit(*values())98 99def delete():100    shop.delete(fields['name'].get().strip())101 102def sell():103    shop.sell(fields['name'].get().strip(), int(fields['qty'].get()))104 105def search():106    name = fields['name'].get().strip().lower()107    refresh(name)108 109def report():110    count, income = shop.report()111    messagebox.showinfo('گزارش فروش', f'تعداد فروخته‌شده: {count}\nدرآمد کل فروش: {income:,.0f}')

روی شماره‌ی هر خط بزن تا به توضیحش بروی؛ روی «خط …» در توضیح بزن تا همان خط در کد روشن شود.

توضیح خط به خط

خط ۷۷def values():
تابع values چهار مقدار فرم را می‌خواند و با نوع درست برمی‌گرداند.
خط ۷۸name = fields['name'].get().strip()
get() متن جعبه‌ی نام را می‌دهد و strip() فاصله‌های اول و آخر را حذف می‌کند تا «مداد » و «مداد» یکی حساب شوند.
خط ۷۹buy = float(fields['buy'].get())
متن قیمت خرید را با float به عدد اعشاری تبدیل می‌کند. اگر متن عدد نباشد (یا خالی باشد) ValueError رخ می‌دهد که در run (خط ۸۸) گرفته می‌شود.
خط ۸۰sell = float(fields['sell'].get())
همین کار برای قیمت فروش.
خط ۸۱qty = int(fields['qty'].get())
تعداد با int به عدد صحیح تبدیل می‌شود، پس «2.5» خطا می‌دهد.
خط ۸۲return name, buy, sell, qty
چهار مقدار را به‌شکل یک تاپل برمی‌گرداند. ترتیب دقیقاً با پارامترهای Shop.add (خط ۲۸) و Shop.edit (خط ۳۶) یکی است.
خط ۸۳
خط خالی.
خط ۸۴def run(action):
تابع مرکزی run. ورودی‌اش action خودش یک تابع است؛ در پایتون می‌شود تابع را مثل هر مقدار دیگری به تابع دیگر داد. همه‌ی دکمه‌ها از طریق run کار می‌کنند (خط ۱۳۴).
خط ۸۵try:
شروع بلوک try.
خط ۸۶action()
تابعی را که گرفته اجرا می‌کند، مثلاً add().
خط ۸۷if action is not search: refresh()
اگر کار بدون خطا تمام شد، جدول را با داده‌ی تازه پر می‌کند (خط ۱۱۳). اگر خطا رخ دهد این خط اجرا نمی‌شود. is not بررسی می‌کند که action همان تابع search (خط ۱۰۵) نباشد؛ چون search خودش جدول را فیلترشده پر کرده، اگر اینجا دوباره refresh() بدون فیلتر اجرا شود نتیجه‌ی جست‌وجو پاک می‌شود. نوشتن if و دستورش در یک خط مجاز است وقتی بدنه فقط یک دستور باشد.
خط ۸۸except (ValueError, TypeError) as exc:
خطاهای ورودی و منطقی را می‌گیرد: همه‌ی raise ValueErrorهای کلاس و خطای تبدیل در خط ۷۹–خط ۸۱. as exc خود شیء خطا را در exc می‌گذارد.
خط ۸۹messagebox.showerror('خطا', str(exc))
پنجره‌ی خطا با عنوان «خطا» و متن پیام خطا را نشان می‌دهد. str(exc) همان پیام فارسی داخل raise است.
خط ۹۰except OSError as exc:
خطاهای فایل، مثلاً وقتی save (خط ۱۵) اجازه‌ی نوشتن نداشته باشد.
خط ۹۱messagebox.showerror('خطای فایل', str(exc))
پنجره‌ی خطا با عنوان «خطای فایل».
خط ۹۲
خط خالی.
خط ۹۳def add():
تابع دکمه‌ی «افزودن کالا». هم‌نام متد Shop.add است ولی تداخلی ندارند: یکی تابع سراسری است و دیگری ویژگی شیء shop.
خط ۹۴shop.add(*values())
values() تاپل چهارتایی می‌دهد و ستاره * آن را باز می‌کند تا چهار آرگومان جدا به shop.add برسد. یعنی همان shop.add(name, buy, sell, qty).
خط ۹۵
خط خالی.
خط ۹۶def edit():
تابع دکمه‌ی «ویرایش کالا».
خط ۹۷shop.edit(*values())
همان الگو، با shop.edit.
خط ۹۸
خط خالی.
خط ۹۹def delete():
تابع دکمه‌ی «حذف کالا».
خط ۱۰۰shop.delete(fields['name'].get().strip())
برای حذف فقط نام لازم است، پس به‌جای values() مستقیم متن جعبه‌ی نام خوانده می‌شود. به همین دلیل خالی بودن قیمت‌ها اینجا خطا نمی‌دهد.
خط ۱۰۱
خط خالی.
خط ۱۰۲def sell():
تابع دکمه‌ی «ثبت فروش».
خط ۱۰۳shop.sell(fields['name'].get().strip(), int(fields['qty'].get()))
نام و تعداد را می‌خواند و به shop.sell می‌دهد. توجه کن که جعبه‌ی «تعداد» هم برای موجودی کالا و هم برای تعداد فروش استفاده می‌شود.
خط ۱۰۴
خط خالی.
خط ۱۰۵def search():
تابع دکمه‌ی «جست‌وجو».
خط ۱۰۶name = fields['name'].get().strip().lower()
متن جعبه‌ی نام را می‌خواند و با lower() کوچک می‌کند تا جست‌وجوی نام‌های انگلیسی به بزرگی و کوچکی حروف حساس نباشد. حروف فارسی بزرگ و کوچک ندارند.
خط ۱۰۷refresh(name)
جدول را فقط با کالاهایی پر می‌کند که این متن در نامشان هست (خط ۱۱۳).
خط ۱۰۸
خط خالی.
خط ۱۰۹def report():
تابع دکمه‌ی «گزارش فروش».
خط ۱۱۰count, income = shop.report()
shop.report() تاپل دوتایی برمی‌گرداند (خط ۶۳) که مستقیم در count و income باز می‌شود.
خط ۱۱۱messagebox.showinfo('گزارش فروش', f'تعداد فروخته‌شده: {count}\nدرآمد کل فروش: {income:,.0f}')
پنجره‌ی اطلاعات با یک f-string. \n خط جدید است و {income:,.0f} عدد را با جداکننده‌ی هزارگان و بدون رقم اعشار می‌نویسد، مثلاً 80,000,000,000.

یادداشت

در نسخه‌ی قبلی، خط ۸۷ فقط refresh() بود و دکمه‌ی «جست‌وجو» کار نمی‌کرد: search() جدول را فیلتر می‌کرد (خط ۱۰۷) ولی بلافاصله refresh() بدون فیلتر همه‌ی کالاها را برمی‌گرداند. شرط if action is not search این مشکل را حل کرده است. شرط در همان یک خط نوشته شده تا شماره‌ی خط‌های بعدی تغییر نکند.

ارتباط با بقیه‌ی کد

همچنین ببینید

values()
در add خط ۹۴ و edit خط ۹۷.
run(action)
در خط ۱۳۴ برای هر هفت دکمه. خودش refresh را در خط ۸۷ صدا می‌زند، به‌جز برای دکمه‌ی جست‌وجو.
add, edit, delete, sell, search, report
در لیست buttons (خط ۱۲۹ و خط ۱۳۰) کنار متن دکمه‌ها قرار می‌گیرند.
توابع کلاس
add ← Shop.add خط ۲۸، edit ← Shop.edit خط ۳۶، delete ← Shop.delete خط ۴۴، sell ← Shop.sell خط ۵۰، report ← Shop.report خط ۶۲.
messagebox
در خط ۴ وارد شده است.

سؤال‌های تمرینی

اول خودت جواب بده، بعد جواب را باز کن.

  1. ستاره‌ی * در shop.add(*values()) چه کار می‌کند؟

    نمایش جواب

    تاپل چهارتایی را به چهار آرگومان جدا باز می‌کند: shop.add(name, buy, sell, qty).

  2. کاربر در قیمت abc می‌نویسد و «افزودن» را می‌زند. مسیر اجرا را دنبال کن.

    نمایش جواب

    خط ۷۹ خطای ValueError می‌دهد، خط ۸۷ اجرا نمی‌شود، خط ۸۸ خطا را می‌گیرد و خط ۸۹ نشانش می‌دهد. چیزی اضافه نمی‌شود.

  3. چرا delete از values() استفاده نمی‌کند؟

    نمایش جواب

    برای حذف فقط نام لازم است و values() با خالی بودن قیمت‌ها خطا می‌داد.

  4. بدون شرط خط ۸۷، دکمه‌ی جست‌وجو چه می‌کرد؟

    نمایش جواب

    عملاً هیچ: search جدول را فیلتر می‌کند و بلافاصله refresh() همه‌ی کالاها را دوباره نشان می‌دهد.

  5. {income:,.0f} برای 1234567.8 چه چاپ می‌کند؟

    نمایش جواب

    1,234,568 — با جداکننده‌ی هزارگان و گرد شده بدون اعشار.