Multi-Window Apps with tkinter (tkinter နဲ့ window အများ ပါတဲ့ app များ)

စာမေးပွဲ ခေါင်းစဉ် မဟုတ်ဘဲ IA အတွက် အသုံးဝင်တဲ့ tool ပါ: IA (IB သင်တန်းမှာ ကျောင်းသားတစ်ယောက်ချင်း ကိုယ်တိုင် တည်ဆောက်ရပြီး ကျောင်းက ဆရာက အမှတ်ပေးတဲ့ အပိုင်း) အတွက် တည်ဆောက်တဲ့ product (program ဒါမှမဟုတ် app) တော်တော်များများမှာ screen တစ်ခုထက် ပိုပါပါတယ်။ ဥပမာ main screen တစ်ခု၊ record (အချက်အလက် အများကြီး စုထားတဲ့ ဒေတာ တစ်ခု) အသစ် ထည့်ဖို့ ဒါမှမဟုတ် ပြင်ဖို့ form တစ်ခု၊ record တစ်ခုရဲ့ အသေးစိတ်ကို ပြတဲ့ window တစ်ခု စသဖြင့်ပါ။ ဒီစာမျက်နှာက tkinter (Python နဲ့ desktop app တည်ဆောက်တဲ့ library) မှာ screen အများကို ဘယ်လို စီမံမလဲ၊ screen တွေကြား ဒေတာကို ဘယ်လို မျှသုံးမလဲ ဆိုတာ ပြပါတယ်။ tkinter က စာမေးပွဲမှာ မေးတဲ့ အကြောင်းအရာ မဟုတ်ပါဘူး။ IA မှာလည်း screen ဒါမှမဟုတ် window ဘယ်နှစ်ခု ပါရမယ်လို့ မသတ်မှတ်ထားပါဘူး။ ဒါပေမယ့် “Window အများအတွက် method တစ်ခုတည်း” အပိုင်းထဲက open_window method (သီးခြား အလုပ်တစ်ခု လုပ်ပေးတဲ့ code အပိုင်း) က စာမေးပွဲမှာ မေးတဲ့ အယူအဆ တစ်ခုကို လေ့ကျင့်ပေးပါတယ်: parameter (method ကို ပေးလိုက်တဲ့ တန်ဖိုးကို လက်ခံတဲ့ variable) ပါတဲ့ function တွေနဲ့ modularisation (သီးခြား အပိုင်းငယ်တွေ ခွဲရေးခြင်း) ရဲ့ အကျိုးကျေးဇူးတွေပါ (IB syllabus ရဲ့ B2.3.4)။ Java နဲ့ ရေးနေသလား။ ဒီစာမျက်နှာရဲ့ Java version က Multi-Window Apps ပါ။


ခြုံငုံ မိတ်ဆက်

IA product တော်တော်များများမှာ screen အများ ပါပါတယ်၊ ဥပမာ:

  • List ဒါမှမဟုတ် အကျဉ်းချုပ်ကို ပြတဲ့ main screen (အဓိက screen)
  • Record အသစ် ထည့်ဖို့ ဒါမှမဟုတ် ရှိပြီးသား record ကို ပြင်ဖို့ add or edit form (ထည့်တဲ့ ဒါမှမဟုတ် ပြင်တဲ့ form)
  • Record တစ်ခုရဲ့ အသေးစိတ်ကို ပြတဲ့ details window
  • Settings window ဒါမှမဟုတ် About window

tkinter Basics (tkinter အခြေခံများ) မှာ လေ့လာခဲ့တာကို ပြန်သတိရပါ: program တစ်ခုမှာ tk.Tk() နဲ့ ဖန်တီးတဲ့ main window တစ်ခုပဲ ရှိပြီး tk.Frame က widget (button၊ text box စတဲ့ control တွေ) အစု တစ်စုကို ထိန်းထားပါတယ်။ ဒီစာမျက်နှာမှာ screen တစ်ခုထက် ပိုပြနည်း နှစ်မျိုးကို လေ့လာပါမယ်: window တစ်ခုတည်းထဲမှာ frame တွေကို လဲပြတာ (နည်းလမ်း 1)၊ ဒါမှမဟုတ် tk.Toplevel တစ်ခုစီ ဖြစ်တဲ့ window အပိုတွေ ဖွင့်တာ (နည်းလမ်း 2) ပါ။ ဘယ်နည်းကို သုံးသုံး ဒေတာ တစ်ခုတည်းနဲ့ အလုပ်လုပ်တဲ့ screen တွေက အဲဒီ ဒေတာကို မျှသုံးနိုင်ဖို့ နည်းလမ်း လိုပါတယ်၊ အဲဒါကို နည်းလမ်း 3 မှာ ရှင်းပြထားပါတယ်။ ကိုယ့် product နဲ့ ကိုက်တာကို ရွေးပါ။ စနစ်တကျ စီထားတဲ့ screen တစ်ခုတည်း ပါတဲ့ product လည်း ရပါတယ်။

tk.Tk() ကို တစ်ခုပဲ ဖန်တီးပါ။ Window အပို တိုင်းက main window ရဲ့ အပိုင် ဖြစ်တဲ့ tk.Toplevel တစ်ခုပါ။ ဒုတိယ tk.Tk() တစ်ခုက သီးခြား Tcl interpreter (tkinter ရဲ့ window တွေကို run ပေးတဲ့ engine) တစ်ခုကို ဖန်တီးတာကြောင့် interpreter တစ်ခုအတွက် ဖန်တီးထားတဲ့ widget တွေနဲ့ tkinter variable တွေကို နောက် interpreter တစ်ခုနဲ့ တွဲသုံးလို့ မရပါဘူး။


နည်းလမ်း 1: Window တစ်ခုတည်းထဲမှာ screen တွေကို လဲပြခြင်း

အရိုးရှင်းဆုံး နည်းက window တစ်ခုတည်းကို ထားပြီး အဲဒီ window ထဲမှာ ပြတာကို ပြောင်းတာပါ။ Screen တစ်ခုစီက Frame တစ်ခုထဲမှာ သူ့ widget တွေကို တည်ဆောက်တဲ့ class (object တွေ ဖန်တီးဖို့ ပုံစံခွက်) တစ်ခုပါ။ Screen ပြောင်းဖို့ program က လက်ရှိ frame ကို pack_forget() နဲ့ window ထဲကနေ ထုတ်ပြီး နောက် frame ကို pack (window ထဲမှာ နေရာချ) လုပ်ပါတယ်။ မပြထားတဲ့ အချိန်မှာလည်း frame တွေနဲ့ သူတို့ထဲ ရိုက်ထည့်ထားတာတွေက ဆက်ရှိနေပါတယ်။

import tkinter as tk


class HomeScreen():
    def __init__(self, window, app):
        self.frame = tk.Frame(window)
        tk.Label(self.frame, text="Home").pack(pady=10)
        tk.Button(self.frame, text="Go to Settings", command=app.show_settings).pack()


class SettingsScreen():
    def __init__(self, window, app):
        self.frame = tk.Frame(window)
        tk.Label(self.frame, text="Settings").pack(pady=10)
        tk.Button(self.frame, text="Back to Home", command=app.show_home).pack()


class App():
    def __init__(self, window):
        window.title("My App")
        self._home = HomeScreen(window, self)
        self._settings = SettingsScreen(window, self)
        self._current = self._home
        self._current.frame.pack(fill="both", expand=True)

    def show_screen(self, screen):
        self._current.frame.pack_forget()    # take the current screen out of the window
        self._current = screen
        screen.frame.pack(fill="both", expand=True)

    def show_home(self):
        self.show_screen(self._home)

    def show_settings(self):
        self.show_screen(self._settings)


window = tk.Tk()
app = App(window)
window.mainloop()

Screen တစ်ခုစီကို App object (class တစ်ခုကနေ ဖန်တီးထားတဲ့ instance) ကို ပေးထားလို့ သူ့ button တွေက app.show_settings ဒါမှမဟုတ် app.show_home ကို ခေါ်နိုင်ပါတယ်။ fill="both", expand=True က window ရဲ့ အရွယ်အစား ပြောင်းတဲ့အခါ frame က window အပြည့် ဖြစ်နေအောင် လုပ်ပေးပါတယ်။


နည်းလမ်း 2: Toplevel နဲ့ Popup Windows (main window ရှေ့မှာ ပွင့်လာတဲ့ သီးခြား window များ)

Add Student လို main screen ရဲ့ အပေါ်မှာ ပွင့်လာမယ့် form အတွက် tk.Toplevel တစ်ခု ဖန်တီးပါ: main window ရဲ့ အပိုင် ဖြစ်တဲ့ window အသစ် တစ်ခုပါ။ ဒီမှာ popup က class တစ်ခု ဖြစ်ပြီး main screen က သူ့ကို စောင့်နိုင်အောင် သူ့ window ကို window attribute (object တစ်ခုရဲ့ ဒေတာကို သိမ်းထားတဲ့ variable) ထဲမှာ သိမ်းထားပါတယ်။

Popup window တစ်ခု ဖန်တီးခြင်း

import tkinter as tk
from models import Student


class AddStudentWindow():
    def __init__(self, parent, model):
        self._model = model
        self._saved = False

        self.window = tk.Toplevel(parent)
        self.window.title("Add Student")
        self.window.transient(parent)    # belongs to the main window (most systems keep it in front)

        tk.Label(self.window, text="Name").grid(row=0, column=0, sticky="w", padx=8, pady=4)
        self._name_entry = tk.Entry(self.window, width=25)
        self._name_entry.grid(row=0, column=1, padx=8, pady=4)
        tk.Label(self.window, text="Grade (0-100)").grid(row=1, column=0, sticky="w", padx=8, pady=4)
        self._grade_entry = tk.Entry(self.window, width=25)
        self._grade_entry.grid(row=1, column=1, padx=8, pady=4)
        self._status = tk.Label(self.window, text="")
        self._status.grid(row=2, column=0, columnspan=2)
        tk.Button(self.window, text="Save", command=self.handle_save).grid(row=3, column=0, pady=8)
        tk.Button(self.window, text="Cancel", command=self.window.destroy).grid(row=3, column=1, pady=8)

        self.window.wait_visibility()    # wait until it is on screen (some systems need this first)
        self._name_entry.focus_set()     # put the typing cursor in the first box
        self.window.grab_set()           # modal: the main window cannot be used while it is open

    def handle_save(self):
        try:
            grade = int(self._grade_entry.get().strip())
        except ValueError:
            self._status.config(text="Grade must be a whole number.")
            return
        try:
            student = Student(self._name_entry.get().strip(), grade)
        except ValueError as error:
            self._status.config(text=str(error))    # the Model's own message
            return
        self._model.add_student(student)
        self._saved = True
        self.window.destroy()    # close the popup

    def is_saved(self):
        return self._saved

Student က Model (app ရဲ့ ဒေတာနဲ့ စည်းမျဉ်းတွေကို ထိန်းထားတဲ့ class တွေ) class ဖြစ်ပြီး MVC with tkinter (app ကို Model၊ View၊ Controller ဆိုပြီး အပိုင်းသုံးပိုင်း ခွဲရေးတဲ့ MVC ပုံစံကို tkinter နဲ့ သုံးခြင်း) မှာ လေ့လာခဲ့တဲ့ class ပါ။ models.py ထဲမှာ ထားပါတယ်။

အဓိက အယူအဆများ

Code ဘာလုပ်သလဲ
tk.Toplevel(parent) Main window ရဲ့ အပိုင် ဖြစ်တဲ့ window အသစ် တစ်ခုကို ဖန်တီးပါတယ်။ Main window ပိတ်တဲ့အခါ သူလည်း ပိတ်သွားပါတယ်။
transient(parent) Popup ကို main window ရဲ့ အပိုင် အဖြစ် မှတ်ထားလို့ system အများစုက popup ကို main window ရဲ့ ရှေ့မှာ ထားပေးပါတယ်။
grab_set() Popup ကို modal ဖြစ်အောင် လုပ်ပါတယ်: popup မပိတ်မချင်း user က main window ကို မသုံးနိုင်ပါဘူး။ wait_visibility() က popup screen ပေါ် ရောက်တဲ့အထိ အရင် စောင့်ပါတယ်၊ တချို့ system တွေမှာ grab မလုပ်ခင် ဒါ လိုပါတယ်။ focus_set() က typing cursor (စာရိုက်မယ့် နေရာကို ပြတဲ့ အမှတ်အသား) ကို popup ရဲ့ ပထမ box ထဲ ထည့်ပေးပါတယ်။
parent.wait_window(popup.window) Popup ကို ဖွင့်ခဲ့တဲ့ code ကို popup မပိတ်မချင်း ခဏ ရပ်ထားပါတယ် (အောက်က main window ကို ကြည့်ပါ)။
destroy() Window ကို ဖျက်ဆီးပါတယ်: window ပိတ်သွားပြီး နောက်ထပ် ပြန်ပြလို့ မရတော့ပါဘူး။

Main window ကနေ popup ကို ဖွင့်ခြင်း

def handle_add(self):
    popup = AddStudentWindow(self._window, self._model)
    self._window.wait_window(popup.window)    # pause here until the popup closes
    self.refresh_display()

wait_window() က popup မပိတ်မချင်း handle_add ကို ခဏ ရပ်ထားပါတယ်၊ ဒါပေမယ့် program က popup ကိုယ်တိုင်ရဲ့ click တွေကို ဆက် တုံ့ပြန်နေပါတယ်။ ဒါကြောင့် user က save လုပ်ပြီး ဒါမှမဟုတ် cancel လုပ်ပြီးမှ refresh_display() က run ပြီး ကျောင်းသား အသစ်က list ထဲမှာ ရှိနေပြီးသား ဖြစ်ပါတယ်။

wait_window() မပါရင် handle_add က ချက်ချင်း ဆက် run ပြီး popup ပွင့်နေတုန်း၊ user က ဘာမှ မရိုက်ထည့်ရသေးခင်မှာ list ကို refresh လုပ်လိုက်ပါလိမ့်မယ်။ ဒါကြောင့် ကျောင်းသား အသစ်က နောက်တစ်ကြိမ် refresh လုပ်တဲ့အထိ ပေါ်လာမှာ မဟုတ်ပါဘူး။


နည်းလမ်း 3: Shared Model (screen တွေ မျှသုံးတဲ့ Model တစ်ခုတည်း)

အပေါ်က နည်းလမ်း နှစ်ခုလုံးမှာ screen တွေကြား ဒေတာ မျှသုံးဖို့ နည်းလမ်း လိုပါတယ်။ ရှင်းလင်းတဲ့ နည်းတစ်ခုက shared Model ပါ: လိုအပ်တဲ့ screen တိုင်းကို Model object တစ်ခုတည်း ကိုပဲ ပေးပါ။ အပေါ်က AddStudentWindow ကို main window ရဲ့ StudentList ကို ပေးထားလို့ သူ ထည့်တဲ့ ကျောင်းသားက main window ပြတဲ့ list ထဲ ရောက်သွားပါတယ်။

flowchart TD
    A["Main program<br>creates the Model once"] -- "passes model to" --> B["Main window"]
    B -- "passes the same model to" --> C["AddStudentWindow"]
    B -- "model" --> M[("StudentList<br>(the same object)")]
    C -- "model" --> M

ပုံထဲက စာတွေက ဒီလို ဆိုလိုပါတယ်: main program က Model ကို တစ်ကြိမ်ပဲ ဖန်တီးပါတယ် (creates the Model once)၊ ပြီးရင် model ကို main window ဆီ ပေးပါတယ် (passes model to)၊ main window က အဲဒီ model တစ်ခုတည်းကို AddStudentWindow ဆီ ဆက်ပေးပါတယ် (passes the same model to)။ Window နှစ်ခုလုံးက StudentList object တစ်ခုတည်း (the same object) ကို သုံးပါတယ်။

# In the main program:
model = StudentList()    # one StudentList for the whole app
manager = StudentManager(window, model)

# In StudentManager, when opening the popup:
popup = AddStudentWindow(self._window, self._model)    # the same StudentList object

Screen နှစ်ခုလုံးက StudentList object တစ်ခုတည်း ကို ညွှန်းနေလို့ (Object References မှာလို object တစ်ခုကို ညွှန်းတဲ့ reference (object ရှိရာ memory နေရာကို ညွှန်းတဲ့ တန်ဖိုး) နှစ်ခုပါ)၊ popup ထဲမှာ ထည့်လိုက်တဲ့ ကျောင်းသားက main window က refresh လုပ်တဲ့အခါ ဖတ်တဲ့ list ထဲမှာ ရှိနေပြီးသား ဖြစ်ပါတယ်။

Screen တစ်ခုစီထဲမှာ Model အသစ် မဖန်တီးပါနဲ့။ ဒီလို လုပ်ရင် screen တစ်ခုစီမှာ သီးခြား ဒေတာ ရှိသွားပြီး တစ်ခုမှာ ပြောင်းလိုက်တာ နောက်တစ်ခုမှာ ပေါ်မလာပါဘူး။

နောက်ထပ် နည်းတစ်ခု: app တစ်ခုလုံးအတွက် မျှသုံးတဲ့ controller တစ်ခု။ တချို့ app တွေက screen တိုင်း လိုတဲ့ ဒေတာကို singleton (instance တစ်ခုတည်းပဲ ရှိတဲ့ class ပုံစံ) ထဲမှာ သိမ်းထားပါတယ်။ Singleton ဆိုတာ ကိုယ့်ရဲ့ object တစ်ခုပဲ ဖန်တီးခွင့်ပေးတဲ့ class ပါ။ ဘယ် screen ကမဆို AppController.get_instance() နဲ့ အဲဒီ object ကို ယူသုံးနိုင်လို့ ဘာမှ ပေးပို့စရာ မလိုပါဘူး။ ဒါပေမယ့် ဘယ် class ကမဆို ဒေတာကို ယူပြီး ပြောင်းနိုင်တာကြောင့် အပြောင်းအလဲ တစ်ခု ဘယ်ကလာလဲ ဆိုတာ မြင်ရ ခက်သွားပါတယ်။ MVC with tkinter: One Controller for Every Screen (screen အားလုံးအတွက် controller (user လုပ်တာတွေကို ကိုင်တွယ်ပြီး Model နဲ့ screen တွေကို ချိတ်ပေးတဲ့ class) တစ်ခုတည်း) မှာ ဥပမာ တစ်ခု ပြထားပါတယ်။ အပေါ်မှာ ပြခဲ့သလို Model ကို လိုတဲ့ screen တွေဆီ ပေးတာက ဘယ် screen က ဘယ် ဒေတာကို သုံးလဲ ဆိုတာ ရှင်းရှင်းလင်းလင်း မြင်ရစေပါတယ်။


Popup ကို ဒေတာ ပေးပို့ခြင်း

တခါတလေ popup က record တစ်ခုကို သီးသန့် လိုပါတယ်: ဥပမာ ကျောင်းသား တစ်ယောက်ရဲ့ အသေးစိတ်တွေ ဖြည့်ပြီးသားနဲ့ ပွင့်လာတဲ့ Edit Student window မျိုးပါ။ Main window က ကျောင်းသားကို ရှာပြီး popup ကို ပေးပါတယ်:

class EditStudentWindow():
    def __init__(self, parent, student):
        self._student = student    # the same Student object that is in the list
        self._saved = False

        self.window = tk.Toplevel(parent)
        self.window.title("Edit Student")
        self.window.transient(parent)    # belongs to the main window (most systems keep it in front)

        tk.Label(self.window, text="Name: " + student.get_name()).pack(padx=10, pady=4)
        self._grade_entry = tk.Entry(self.window, width=10)
        self._grade_entry.insert(0, str(student.get_grade()))    # start with the current grade
        self._grade_entry.pack(padx=10, pady=4)
        self._status = tk.Label(self.window, text="")
        self._status.pack()
        tk.Button(self.window, text="Save", command=self.handle_save).pack(pady=8)

        self.window.wait_visibility()
        self._grade_entry.focus_set()
        self.window.grab_set()

    def handle_save(self):
        try:
            self._student.set_grade(int(self._grade_entry.get().strip()))
        except ValueError:
            self._status.config(text="Enter a whole number from 0 to 100.")
            return
        self._saved = True
        self.window.destroy()

    def is_saved(self):
        return self._saved

Popup ကို StudentList ထဲမှာ ရှိတဲ့ Student object အတိုင်းကိုပဲ ပေးထားလို့ popup ထဲမှာ ပြင်လိုက်တာက list ထဲက ကျောင်းသားကို ပြင်လိုက်တာ ဖြစ်ပြီး main window က refresh လုပ်ရုံပဲ လိုပါတယ်။ int() က ဂဏန်း မဟုတ်တဲ့ စာသားကို ValueError ဆိုတဲ့ exception (program run နေစဉ် ဖြစ်တဲ့ အမှား) နဲ့ လက်မခံသလိုပဲ set_grade() ကလည်း 0 ကနေ 100 အပြင်က grade ကို ValueError နဲ့ လက်မခံပါဘူး။ ဒါကြောင့် except တစ်ခုတည်းက နှစ်ခုလုံးကို ကိုင်တွယ်ပေးပါတယ်။

Popup ကနေ အဖြေ ပြန်ယူခြင်း

တခါတလေ user က popup ထဲမှာ ဘာလုပ်ခဲ့လဲ (ဥပမာ Save ကို နှိပ်ခဲ့လား၊ save မလုပ်ဘဲ popup ကို ပိတ်ခဲ့လား) ဆိုတာ main window က သိဖို့ လိုပါတယ်။ Popup ရဲ့ Save handler (button နှိပ်တာလို event ဖြစ်တဲ့အခါ run တဲ့ code) က _saved ကို True လို့ သတ်မှတ်ပြီး is_saved() က အဲဒီ တန်ဖိုးကို ပြန်ပေးပါတယ်။ Main window က popup object ကို ဆက်ကိုင်ထားလို့ wait_window() ပြန်လာပြီးတာနဲ့ မေးကြည့်နိုင်ပါတယ်:

def handle_edit(self):
    selected = self._model.find_by_name(self._name_entry.get().strip())
    if selected == None:
        self._status.config(text="No student with that name.")
        return

    popup = EditStudentWindow(self._window, selected)
    self._window.wait_window(popup.window)

    if popup.is_saved() == True:
        self._status.config(text="Changes saved.")
    else:
        self._status.config(text="No changes made.")
    self.refresh_display()

Window အများအတွက် method တစ်ခုတည်း

App မှာ window တွေ များလာတာနဲ့အမျှ window ဖွင့်တဲ့ code တွေ ထပ်နေလာပါတယ်။ အောက်က window class တစ်ခုစီက သူ့ window ကို ပုံစံတူ တည်ဆောက်တာကြောင့် class နဲ့ title ကို parameter အဖြစ် လက်ခံတဲ့ method တစ်ခုက ဘယ် window ကိုမဆို ဖွင့်နိုင်ပါတယ်။ ဒါက B2.3.4 ထဲက အယူအဆပါ: function ဆိုတာ မတူတဲ့ input (program ထဲ ထည့်ပေးတဲ့ ဒေတာ) တွေနဲ့ ခေါ်သုံးနိုင်ပြီး ပြန်သုံးလို့ရတဲ့ code အပိုင်း တစ်ခုပါ။

class HelpWindow():
    def __init__(self, parent):
        self.window = tk.Toplevel(parent)
        tk.Label(self.window, text="Type a name and a grade, then click Add.").pack(padx=20, pady=20)


class AboutWindow():
    def __init__(self, parent):
        self.window = tk.Toplevel(parent)
        tk.Label(self.window, text="Student Manager, version 1").pack(padx=20, pady=20)


class MainWindow():
    def __init__(self, window):
        self._window = window
        self._open = {}    # each open window, by its title
        tk.Button(window, text="Help", command=self.handle_help).pack(padx=20, pady=4)
        tk.Button(window, text="About", command=self.handle_about).pack(padx=20, pady=4)

    def open_window(self, window_class, title):
        if title in self._open and self._open[title].window.winfo_exists() == 1:
            self._open[title].window.deiconify()    # already open: show it if it was minimised
            self._open[title].window.lift()         # and bring it to the front
        else:
            new_window = window_class(self._window)
            new_window.window.title(title)
            self._open[title] = new_window

    def handle_help(self):
        self.open_window(HelpWindow, "Help")

    def handle_about(self):
        self.open_window(AboutWindow, "About")

open_window ကို class တစ်ခု (HelpWindow ဒါမှမဟုတ် AboutWindow) ပေးလိုက်ပြီး အဲဒီ class ကို ခေါ်ကာ window ကို ဖန်တီးတာကြောင့် method တစ်ခုတည်းက window နှစ်ခုလုံးကို ဖွင့်ပေးပါတယ်။ သူ ဖွင့်ထားတဲ့ window တွေကို title အလိုက် dictionary (key နဲ့ value တွဲပြီး သိမ်းတဲ့ ဒေတာ အစု) တစ်ခုထဲမှာလည်း မှတ်ထားပါတယ်။ Help ကို ဒုတိယအကြိမ် နှိပ်ရင် ဒုတိယ copy တစ်ခု မဖွင့်ဘဲ Help window ကို ပြန်ပြပြီး (minimise (window ကို taskbar ထဲ ချုံ့ထား) လုပ်ထားရင်တောင်) ရှေ့ဆုံးကို ယူလာပါတယ်။ winfo_exists() က window ရှိနေသရွေ့ (minimise လုပ်ထားတာ၊ ဖျောက်ထားတာ အပါအဝင်) 1 ကို ပြန်ပေးပြီး destroy() နဲ့ ဒါမှမဟုတ် close button နဲ့ ပိတ်ပြီးသွားရင် 0 ကို ပြန်ပေးပါတယ်။ ဒါကြောင့် ပိတ်ပြီးသား window ကို အသစ် ပြန်ဖွင့်ပါတယ်။

ဒီလို ရေးတာရဲ့ အကျိုးကျေးဇူးတွေ (Methods မှာ လေ့လာခဲ့တဲ့ modularisation ရဲ့ အကျိုးကျေးဇူးတွေ) ပါ:

  • Window အသစ် တစ်ခုအတွက် သူ့ handler ထဲမှာ line တစ်ကြောင်းပဲ လိုပါတယ်။
  • Window အားလုံး ပွင့်ပုံကို ပြောင်းချင်ရင် (ဥပမာ window အားလုံးကို အရွယ်အစား တူအောင် လုပ်ချင်ရင်) နေရာ တစ်ခုတည်းမှာပဲ ပြင်ရပါတယ်။
  • Handler တစ်ခုစီက ဘာလုပ်လဲ ဆိုတာ line တစ်ကြောင်းတည်းနဲ့ ပြောပြပါတယ်။

Add Student form လို ဒေတာ လိုတဲ့ window ကိုတော့ main window က Model ကို ပေးနိုင်ပြီး popup ကို စောင့်နိုင်အောင် နည်းလမ်း 2 မှာလိုပဲ ဆက်ဖွင့်ရပါတယ်။


Window ကို ပိတ်ခြင်း၊ ဖျောက်ခြင်းနဲ့ ပြန်ပြခြင်း

Code ဘာလုပ်သလဲ
destroy() Window ကို ဖျက်ဆီးပါတယ်: window ပိတ်သွားပြီး သူ့ widget တွေ (ထဲမှာ ရိုက်ထည့်ထားတာတွေ အပါအဝင်) ပျောက်သွားပါတယ်။
withdraw() Window ကို ဖျောက်ထားပါတယ်။ ထဲမှာ ရိုက်ထည့်ထားတာ အားလုံးနဲ့အတူ window က ဆက်ရှိနေပါတယ်။
deiconify() ဖျောက်ထားတဲ့ window ကို ပြန်ပြပါတယ်။
lift() Window တစ်ခုကို တခြား window တွေရဲ့ ရှေ့ကို ယူလာပါတယ်။
protocol("WM_DELETE_WINDOW", method) User က window ရဲ့ close button (X) ကို နှိပ်တဲ့အခါ window ကို ချက်ချင်း မပိတ်ဘဲ ကိုယ့် method ကို run ပါတယ်။

Main window ကို ပြန်ပြခြင်း။ တခြား window တစ်ခု ပွင့်နေစဉ် main window ကို ဖျောက်ထားပြီး အဲဒီ window ပိတ်တဲ့အခါ ပြန်ပြနိုင်ပါတယ်:

def show_help_instead(self):
    self._help = HelpWindow(self._window)
    self._help.window.title("Help")
    self._help.window.protocol("WM_DELETE_WINDOW", self.close_help)    # runs when its X is clicked
    self._window.withdraw()    # hide the main window

def close_help(self):
    self._help.window.destroy()
    self._window.deiconify()   # show the main window again

ဖျောက်ထားတဲ့ main window က app ကို ဆက် run နေစေပါတယ်။ Main window ကို withdraw လုပ်ထားပြီး ဘာကမှ ပြန်မပြပေးရင် program က screen ပေါ်မှာ window မရှိတော့ပေမယ့် ဆက် run နေပါတယ်။ နောက်ဆုံး မြင်ရတဲ့ window ပိတ်သွားတိုင်း main window ကို ပြန်ပြပါ၊ ဒါမှမဟုတ် app ကို ရပ်ပါ။

App ကို ပိတ်ခြင်း။ Main window ပေါ်မှာ destroy() ကို ခေါ်ရင် mainloop() ပြီးဆုံးသွားပြီး Toplevel အားလုံးကိုလည်း တစ်ပါတည်း ပိတ်ကာ app ရပ်သွားပါတယ်။ မထွက်ခင် မေးချင်ရင် main window ကို protocol("WM_DELETE_WINDOW", ...) နဲ့ method တစ်ခု ပေးပါ။ အဲဒီ method က messagebox.askyesno နဲ့ မေးပြီး user က Yes ကို နှိပ်မှပဲ destroy() ကို ခေါ်ရပါမယ်။


ဥပမာ အပြည့်အစုံ: Add popup ပါတဲ့ Student Manager

Student နဲ့ StudentList က MVC with tkinter ထဲက Model class တွေ ဖြစ်ပြီး models.py ထဲမှာ ထားပါတယ်။ AddStudentWindow က နည်းလမ်း 2 ထဲက popup ဖြစ်ပြီး add_popup.py ထဲမှာ ထားပါတယ်။ File အားလုံးကို folder တစ်ခုတည်းထဲမှာ ထားပါ။

import tkinter as tk
from models import StudentList
from add_popup import AddStudentWindow


class StudentManager():
    def __init__(self, window, model):
        self._window = window
        self._model = model
        window.title("Student Manager")
        self._display = tk.Listbox(window, width=40, height=10)
        self._display.pack(padx=10, pady=10)
        tk.Button(window, text="Add Student", command=self.handle_add).pack()
        self._status = tk.Label(window, text="Ready")
        self._status.pack(pady=(4, 10))
        self.refresh_display()

    def handle_add(self):
        popup = AddStudentWindow(self._window, self._model)
        self._window.wait_window(popup.window)    # pause here until the popup closes
        if popup.is_saved() == True:
            self._status.config(text="Student added.")
        else:
            self._status.config(text="No student added.")
        self.refresh_display()

    def refresh_display(self):
        self._display.delete(0, tk.END)
        students = self._model.get_all()
        for i in range(len(students)):
            self._display.insert(tk.END, str(i + 1) + ". " + str(students[i]))


window = tk.Tk()
model = StudentList()
manager = StudentManager(window, model)
window.mainloop()

ဒီဥပမာက ပြသတာတွေ:

  • tk.Tk() main window တစ်ခုနဲ့ modal Toplevel အဖြစ် ဖွင့်တဲ့ Add Student form
  • Main window က wait_window() နဲ့ popup ကို စောင့်ပြီးမှ ကျောင်းသား save လုပ်ခဲ့လား popup ကို မေးပါတယ်
  • Window နှစ်ခုလုံးက StudentList တစ်ခုတည်းကို မျှသုံးလို့ main window က refresh လုပ်ရုံပဲ လိုပါတယ်
  • Popup က input ကို စစ်ပြီး Model က တန်ဖိုး တစ်ခုကို လက်မခံတဲ့အခါ Model ရဲ့ ကိုယ်ပိုင် message ကို ပြပါတယ်

Quick Check (အမြန် စစ်ဆေးမေးခွန်း)

Q1. App တစ်ခုမှာ main window ရှိပြီးသားပါ။ သီးခြား ဒုတိယ window တစ်ခုအတွက် ဘာကို ဖန်တီးသင့်သလဲ။

Q2. Main window တစ်ခုက Add popup ကို ဖွင့်ပြီး သူ့ list ကို ချက်ချင်း refresh လုပ်ပါတယ်။ Popup ထဲမှာ ထည့်တဲ့ ကျောင်းသားတွေက နောက်တစ်ကြိမ် refresh လုပ်တဲ့အထိ မပေါ်ပါဘူး။ အကောင်းဆုံး ပြင်နည်းက ဘာလဲ။

Q3. Popup window တစ်ခု modal ဖြစ်တယ်ဆိုတာ ဘာကို ဆိုလိုသလဲ။

Q4. Help window ပွင့်နေစဉ် program တစ်ခုက သူ့ main window ကို withdraw() နဲ့ ဖျောက်ထားပါတယ်။ Main window အကြောင်း ဘယ်အချက်က မှန်သလဲ။

Q5. ဒီစာမျက်နှာက Add Student popup က သူ့ ကျောင်းသား အသစ်ကို main window ရဲ့ list ထဲ ဘယ်လို ထည့်သလဲ။


Practice Exercises (လေ့ကျင့်ခန်းများ)

(4 marks) စတာတွေက လေ့ကျင့်ခန်း တစ်ခုစီအတွက် ရနိုင်တဲ့ အမှတ်ပါ။ IB စာမေးပွဲ မေးခွန်းတွေမှာ အမှတ်ကို ဒီလို ပြလေ့ ရှိပါတယ်။

Core (အခြေခံ အဆင့်)

  1. Message ပြတဲ့ popup (4 marks): Show Message button ပါတဲ့ main window တစ်ခုကို Construct (ကိုယ်တိုင် တည်ဆောက်ပါ)။ အဲဒီ button ကို နှိပ်ရင် modal popup (Toplevel တစ်ခု) ပွင့်ရပါမယ်။ Popup မှာ “Hello from the popup.” လို့ ရေးထားတဲ့ label တစ်ခုနဲ့ popup ကို ပိတ်ပေးတဲ့ Close button တစ်ခု ပါရပါမယ်။ Popup ပွင့်နေစဉ် user က main window ကို မသုံးနိုင်ရပါဘူး။

  2. Screen swap (4 marks): Window တစ်ခုတည်းထဲမှာ ပြတဲ့ screen နှစ်ခုပါ app တစ်ခုကို Construct လုပ်ပါ: နှုတ်ဆက်စာနဲ့ Go to Settings button ပါတဲ့ Home screen တစ်ခု၊ user ရဲ့ နာမည် ရိုက်ထည့်ဖို့ entry box နဲ့ Back to Home button ပါတဲ့ Settings screen တစ်ခုပါ။ Button တစ်ခုခုကို နှိပ်ရင် window တစ်ခုတည်းထဲမှာ screen ကို လဲပြရပါမယ်။ User က Back to Home ကို နှိပ်တဲ့အခါ Home screen က “Welcome, “ နောက်မှာ Settings screen မှာ ရိုက်ထည့်ခဲ့တဲ့ နာမည်ကို ဆက်ပြရပါမယ်၊ box က ဗလာ ဖြစ်နေရင် “Welcome” ကိုပဲ ပြရပါမယ်။

Extension (တိုးချဲ့ အဆင့်)

  1. Edit popup (6 marks): Popup ကို ဒေတာ ပေးပို့ခြင်း အပိုင်းထဲက Edit Student feature ရဲ့ ကျန်တဲ့ အပိုင်းကို Construct လုပ်ပါ။ ဥပမာ အပြည့်အစုံ (Complete Example) ရဲ့ main window မှာ နာမည် box တစ်ခုနဲ့ Edit button တစ်ခု ထည့်ပါ။ EditStudentWindow က grade အပြင် နာမည်ကိုပါ ပြောင်းနိုင်အောင် ချဲ့ပါ၊ ဘာမှ မပြောင်းဘဲ ပိတ်ပေးတဲ့ Cancel button တစ်ခုလည်း ထည့်ပါ။ Constructor (object ဖန်တီးချိန် attribute တွေကို စတင် သတ်မှတ်ပေးတဲ့ method) လုပ်သလိုပဲ ဗလာ နာမည်ကို လက်မခံတဲ့ set_name method ကို Student ထဲ ထည့်ပါ။ Popup ပိတ်ပြီးနောက် main window ရဲ့ list မှာ ပြောင်းလဲမှုတွေ ပေါ်ရပါမယ်။

  2. မပိတ်ခင် အတည်ပြုခြင်း (6 marks): Student Manager ရဲ့ main window အတွက် ပိတ်ခါနီး စစ်ဆေးချက် တစ်ခုကို Construct လုပ်ပါ: user က window ရဲ့ close button (X) ကို နှိပ်တဲ့အခါ message box (စာတို တစ်ခု ပြတဲ့ window အသေး) တစ်ခုက Yes နဲ့ No button တွေနဲ့အတူ “Exit without saving?” (save မလုပ်ဘဲ ထွက်မှာလား) လို့ မေးရပါမယ်။ Yes ကို နှိပ်ရင် app ကို ပိတ်ပြီး No ကို နှိပ်ရင် ဆက်ဖွင့်ထားရပါမယ်။ protocol("WM_DELETE_WINDOW", ...) နဲ့ messagebox.askyesno ကို သုံးပါ။

Challenge (စိန်ခေါ်မှု အဆင့်)

  1. ပစ္စည်းပျောက် ရုံး (8 marks): ကျောင်းတစ်ကျောင်းရဲ့ ပစ္စည်းပျောက် ရုံး (lost property office) အတွက် window သုံးခုပါ tkinter app တစ်ခုကို Construct လုပ်ပါ:
    • Main window: လာအပ်ထားတဲ့ ပစ္စည်း အားလုံးကို ပိုင်ရှင် လာယူပြီးပြီလား ဆိုတာနဲ့အတူ စာရင်းပြပြီး Log Item နဲ့ Item Details button တွေ ပါရပါမယ်။
    • Log Item popup (modal): ပစ္စည်းရဲ့ ဖော်ပြချက်နဲ့ ပစ္စည်းကို တွေ့ခဲ့တဲ့ နေရာကို လက်ခံပါတယ်။ ဖော်ပြချက် ဒါမှမဟုတ် နေရာ ဗလာ ဖြစ်နေရင် message တစ်ခု ပြပြီး လက်မခံရပါဘူး။
    • Item Details popup (modal): main window မှာ ရွေးထားတဲ့ ပစ္စည်းရဲ့ အချက်အလက် အားလုံးကို ပြပြီး Mark as Collected button ပါရပါမယ်။

    Window သုံးခုလုံးက ပစ္စည်းတွေနဲ့ ပစ္စည်းဆိုင်ရာ စည်းမျဉ်းတွေကို ထိန်းထားတဲ့ LostProperty model object တစ်ခုတည်းနဲ့ အလုပ်လုပ်ရပါမယ်။ Popup ပိတ်တိုင်း main window ရဲ့ list က နောက်ဆုံး အခြေအနေအတိုင်း ဖြစ်နေရပါမယ်။

  2. Update မဖြစ်တဲ့ list ကို ရှင်းပြခြင်း (6 marks): Quiz night (အသင်းလိုက် ပြိုင်ဖြေတဲ့ ပဟေဠိ ညပွဲ) app တစ်ခုမှာ အသင်းတွေကို စာရင်းပြတဲ့ main window နဲ့ Add Team button ပါပါတယ်။ အဲဒီ button ရဲ့ handler က main window ရဲ့ TeamList ကို ပေးပြီး AddTeamWindow တစ်ခုကို ဖန်တီးကာ နောက် line မှာ refresh_display() ကို ခေါ်ပါတယ်။ Popup က အသင်း အသစ်ကို အဲဒီ TeamList ထဲ ထည့်ပြီး ပိတ်သွားပါတယ်။ User က အသင်း တစ်သင်း ထည့်လိုက်ပေမယ့် main window ရဲ့ list မှာ မပေါ်ပါဘူး။ Add Team ကို နှိပ်လိုက်တဲ့ အချိန်ကစပြီး ဘာတွေ ဖြစ်သွားလဲ ဆိုတာကို အစဉ်လိုက် ဖော်ပြရင်း ဘာကြောင့်လဲ ဆိုတာ Explain (ရှင်းပြပါ)။ ပြီးရင် program ကို ပြင်နိုင်တဲ့ မတူတဲ့ နည်းလမ်း နှစ်ခုကို Describe (ဖော်ပြရှင်းပြပါ)။ Code ရေးဖို့ မလိုပါဘူး။

ဆက်စပ် ခေါင်းစဉ်များ

  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ်: tkinter Basics (tkinter အခြေခံများ): main window၊ widget တွေ၊ frame တွေနဲ့ event handling (user က နှိပ်တာ ဒါမှမဟုတ် စာရိုက်တာကို program က တုံ့ပြန်အောင် လုပ်ခြင်း)
  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ်: MVC with tkinter (tkinter နဲ့ MVC): Model class တွေနဲ့ မျှသုံးတဲ့ controller
  • အရင် လေ့လာထားရမယ့် ခေါင်းစဉ်: Object References (object reference များ): shared Model ဆိုတာ object တစ်ခုကို ညွှန်းတဲ့ reference နှစ်ခုပါ
  • ဆက်စပ် ခေါင်းစဉ်: Methods (method များ): parameter ပါတဲ့ function တွေနဲ့ modularisation ရဲ့ အကျိုးကျေးဇူးများ (B2.3.4)
  • Java version: Multi-Window Apps (Window အများ ပါတဲ့ app များ): JavaFX stage တွေ၊ scene တွေနဲ့ အယူအဆ အတူတူကို ပြထားပါတယ်
  • နောက်ထပ် လေ့လာမယ့် ခေါင်းစဉ်: File I/O in Apps (app တွေထဲမှာ file ဖတ်ခြင်း၊ ရေးခြင်း): app ကို ပိတ်ပြီး ပြန်ဖွင့်တဲ့အခါမှာလည်း ဒေတာ ကျန်နေအောင် သိမ်းခြင်း
  • နောက်ထပ် လေ့လာမယ့် ခေါင်းစဉ်: Structuring Your IA (IA ကို ဖွဲ့စည်း တည်ဆောက်ခြင်း): အားလုံးကို ပေါင်းစပ်ခြင်း

© EduCS.me. ကွန်ပျူတာသိပ္ပံ ပညာရေးအတွက် အရင်းအမြစ်တွေ စုထားတဲ့ နေရာပါ။ EduCS is an independent educational resource. It is not affiliated with, or endorsed by, the International Baccalaureate Organization. (EduCS ဟာ သီးခြား ပညာရေး အရင်းအမြစ်တစ်ခုပါ။ International Baccalaureate Organization နဲ့ ဆက်စပ်မှု မရှိသလို သူတို့ရဲ့ ထောက်ခံချက်လည်း မရှိပါဘူး။) ဆက်သွယ်ရန်