Методы программирования tkinter и GUI - PullRequest
3 голосов
/ 16 октября 2011

Надеюсь, это не подпадает под "общую тему обсуждения", так как я хотел бы, чтобы это было скорее решение этих проблем эффективным способом, чем гигантские дебаты о том, какой общий подход к программированию GUI является абсолютным лучшим.

Итак, я начал программирование GUI с tkinter, и вкратце, мой код становится довольно уродливым довольно быстро.Я пытаюсь создать редактор карт на основе плитки для видеоигры.Мои основные проблемы, похоже, заключаются в следующем:

  1. невозможность обратных вызовов для возврата значений.
  2. невозможность передачи данных между окнами легко.

Я предполагаюпричина, по которой я вижу это как проблемы, заключается в том, что я использую функции гораздо больше, чем классы.Например, мое окно «load tileset» обрабатывается полностью функционально: щелчок по опции меню в главном окне вызывает функцию, которая загружает новое окно.Из этого окна я создаю диалог открытия файла при поиске изображения и изменяю холст, отображающий изображение, когда нажимаю клавишу ввода (чтобы он рисовал соответствующую сетку поверх изображения).функция функция функция.

То, что для меня выглядит очень плохой практикой, это включение дополнительных аргументов для компенсации.Например, когда я создаю набор плиток, экземпляр созданного класса TileSet должен быть отправлен обратно в главное окно, где может отображаться соответствующая информация.У меня есть список загруженных наборов плиток в качестве глобальной переменной (еще более плохая практика: все, что связано с моим корневым окном, находится в глобальной области видимости! Yay!), И поскольку функции обратного вызова не возвращают значения, я передаю этот список в качестве аргументамоей функции «загрузить окно набора тайлов», которая затем передает аргумент в функцию создания набора тайлов (вызывается, когда вы нажимаете соответствующую кнопку в окне), где это действительно необходимо, чтобы я мог добавить вновь созданныйтайлсет в список.Передача аргументов через такую ​​«иерархию» функций кажется ужасной идеей.Это сбивает с толку, это ужасно для написания модульного кода и, как правило, кажется ненужным.

Моя попытка исправить проблему - написать класс, представляющий весь графический интерфейс пользователя, и пользовательские классы окон (то есть класс GUI).может создавать и ссылаться), которые действительно могут хранить соответствующие данные.Это должно решить проблемы с передачей данных между окнами.Надеюсь, это сократит мое бесплатное использование лямбда-функций в обратных вызовах.Но мне интересно: это лучший способ?Или хотя бы близко?Я бы предпочел не начинать переписывать, а потом заканчивать с другой системой, которая просто неаккуратна и по-другому сбивает с толку.Я знаю, что мои методы плохие, но я не знаю, какой будет лучший подход.Я получаю много советов о том, как делать конкретные вещи, но не о том, как структурировать программу в целом.Любая помощь будет принята с благодарностью.

1 Ответ

8 голосов
/ 16 октября 2011

Похоже, вы пытаетесь создать графический интерфейс, который действует процедурно, который не будет работать.GUI не являются процедурными, их код не работает линейно, когда функции вызывают обратные вызовы, которые возвращают значения.То, что вы спрашиваете, не уникально для tkinter.Такова природа основанного на событиях программирования GUI - обратные вызовы не могут ничего возвратить, потому что вызывающая сторона - это событие, а не функция.

Грубо говоря, вы должны использовать глобальный объекткакой-то вид для хранения ваших данных.Обычно это называется «Модель».Это может быть глобальная переменная, база данных или какой-либо объект.В любом случае, он должен существовать «глобально»;то есть он должен быть доступен всему графическому интерфейсу.

Часто этот доступ предоставляется третьим компонентом, называемым «Контроллер».Это интерфейс между GUI («Вид») и данными («Модель»).Эти три компонента составляют то, что называется шаблоном модель-представление-контроллер, или MVC.

Модель, вид и контроллер не должны быть тремя разными объектами.Часто графический интерфейс и контроллер - это один и тот же объект.Для небольших программ это работает довольно хорошо - компоненты GUI взаимодействуют напрямую с вашей моделью данных.

Например, у вас может быть класс, представляющий окно, которое наследуется от Tkinter.Toplevel.Он может иметь атрибут, который представляет редактируемые данные.Когда пользователь выбирает «Новый» в главном окне, он делает что-то вроде self.tileset = TileSet(filename).То есть он устанавливает атрибут с именем tileset объекта графического интерфейса с именем self как экземпляр класса TileSet, специфичный для данного имени файла.Более поздние функции, которые манипулируют данными, используют self.tileset для доступа к объекту.Для функций, которые находятся вне объекта главного окна (например, функция «сохранить все» из главного окна), вы можете либо передать этот объект в качестве аргумента, либо использовать объект окна в качестве контроллера, попросив его сделать что-то для его объекта.Tileset.

Вот краткий пример:

import Tkinter as tk
import tkFileDialog
import datetime

class SampleApp(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.windows = []
        menubar = tk.Menu(self)
        self.configure(menu=menubar)
        fileMenu = tk.Menu(self)
        fileMenu.add_command(label="New...", command=self.new_window)
        fileMenu.add_command(label="Save All", command=self.save_all)
        menubar.add_cascade(label="Window", menu=fileMenu)
        label = tk.Label(self, text="Select 'New' from the window menu")
        label.pack(padx=20, pady=40)

    def save_all(self):
        # ask each window object, which is acting both as 
        # the view and controller, to save it's data
        for window in self.windows:
            window.save()

    def new_window(self):
        filename = tkFileDialog.askopenfilename()
        if filename is not None:
            self.windows.append(TileWindow(self, filename))

class TileWindow(tk.Toplevel):
    def __init__(self, master, filename):
        tk.Toplevel.__init__(self, master)
        self.title("%s - Tile Editor" % filename)
        self.filename = filename
        # create an instance of a TileSet; all other
        # methods in this class can reference this
        # tile set
        self.tileset = TileSet(filename)
        label = tk.Label(self, text="My filename is %s" % filename)
        label.pack(padx=20, pady=40)
        self.status = tk.Label(self, text="", anchor="w")
        self.status.pack(side="bottom", fill="x")

    def save(self):
        # this method acts as a controller for the data,
        # allowing other objects to request that the 
        # data be saved
        now = datetime.datetime.now()
        self.status.configure(text="saved %s" % str(now))

class TileSet(object):
    def __init__(self, filename):
        self.data = "..."

if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()
...