Python:GUI 和后端

Python: GUI and back-end

我正在从事一个数据采集项目,使用 python 为不同的仪器(电压表、照相机等)构建多个 data-monitoring/controlling 程序。我正在使用 python3 和 tkinter(由于其开放许可证)作为我的 GUI。

现在每种乐器的基本结构是:

import packages

class all_GUI():
    def __init__():
    
        device = volt_device()

   functions linking GUI elements to HW calls


    mainloop()

class volt_device():
    def __init__():

    functions to access HW functionality


mainapp = all_GUI()

它更有效,但代码中有很多 GUI 和硬件之间的调用 类。如果我想重用代码的 GUI 部分并 link 它与另一个硬件板,我几乎必须重写整个东西。你可以想象这不是很吸引人:-)

我想 class volt_device 可以移动到一个单独的文件中并根据需要加载。但是因为 GUI 从 HW 部分调用许多功能,每个 HW 文件(例如支持不同的板)必须具有完全相同的命名约定。不可怕,但也不是我认为最好的。

我正在考虑尽可能将 GUI 和 HW 分开,但遇到了一些困难。我正在研究模型-视图-控制器模式,但无法使其工作。我的想法是拥有三个程序:

import GUI
import HW

objGUI = 
objHW = 

link functions to interface objects

mainloop()

class GUI():
    def __init__():

    build GUI here with all elements
    (this is getting sticky since I need to define functions to be executed when GUI values change
    or buttons are pushed)

有多个硬件文件支持不同的仪器。

class HW():
   def __init__():
 
   define hardware board, have functions to change/monitor values

理想情况下,我会有一个相对简单的 HW 文件(文件 3)。要拥有全新的虚拟设备,我必须加载 GUI 部分(文件 2;未修改)并编写一个简单的“控制器”(文件 1)linking GUI 元素到硬件功能。听起来很简单...

当我尝试 link GUI 和 HW 一起时,我卡住了。我不确定如何正确处理 GUI 元素并为它们分配适当的 HW call/function。也许整个想法是有缺陷的,GUI/HW 分离需要以不同的方式处理...

我确信这个问题在我找不到它之前就已经解决了……或者现在就解决了。如果您有任何建议 and/or 编码参考,我将不胜感激。

谢谢。

拉多万

...would have to have the exact same naming convention. Not terrible, but not the best either I think.

相反,那可能是最好的方法。本质上,您将创建一个通用接口,并让每个“板”用它的具体细节或 subclass 实现接口。然后你为 tkinter 创建一个 class,它可以从 methodsarguments.

构建一个接口

两个显示都是自动生成的,并且一切都以一种或另一种方式回到最基本的组件。

非常通用和简化的示例:

import tkinter as tk, abc
from typing import List, Tuple, Callable, Iterable, Dict
import inspect


#create formal interface
class IGenericBoard(metaclass=abc.ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        isinterface  = hasattr(subclass, 'read_pin')  and callable(subclass.read_pin)
        isinterface &= hasattr(subclass, 'write_pin') and callable(subclass.write_pin)
        return isinterface

    @abc.abstractmethod
    def generic_pin_read(self, pin:int) -> int:
        raise NotImplementedError

    @abc.abstractmethod
    def generic_pin_write(self, pin:int, data:int):
        raise NotImplementedError


#implement IGenericBoard        
class GenericBoard(IGenericBoard):
    @property
    def model(self):
        #the "model type" for this board instance
        return type(self).__name__
        
    @property
    def prefix(self) -> List:
        #the prefix(es) to use when finding functions
        return self._prefix if isinstance(self._prefix , (List, Tuple)) else [self._prefix]
        
    @property
    def msgvar(self) -> tk.StringVar:
        #the output message var
        return self._msgvar
        
    @property
    def attributes(self) -> Dict:
        #get everything in one shot ~ for **kwargs
        return dict(
            model =self.model ,
            prefix=self.prefix,
            msgvar=self.msgvar,
        )

    def __init__(self):
        self._prefix = 'generic'
        self._msgvar = tk.StringVar()

    def generic_pin_read(self, pin:int) -> int:
        self._msgvar.set(f'reading pin {pin}')
        #... really do this
        return 0

    def generic_pin_write(self, pin:int, data:int):
        self._msgvar.set(f'writing {data} on pin {pin}')
        #... really do this


#"final" class
class LEDBoard(GenericBoard):
    def __init__(self):
        GenericBoard.__init__(self)
        self._prefix = self.prefix + ['led']

    def led_blink_write(self, pin:int=13):
        self.generic_pin_write(pin, 1)
        self._msgvar.set(f'blinking on pin {pin}')
        #... really do this


''' tkBaseBoard
        the baseclass for all "tk[Version]Board" classes
        generates form interfaces for methods with the proper prefix(es)
'''
class tkBaseBoard(tk.Frame):
    def __init__(self, master, model, msgvar, prefix, **kwargs):
        tk.Frame.__init__(self, master, **{'bd':2, 'relief':'raised', **kwargs})
        self.grid_columnconfigure(0, weight=1)

        #board model label
        tk.Label(self, text=model, font="Consolas 12 bold").grid(row=0, column=0, sticky='w')

        #message output from board
        self.output_ent = tk.Entry(self, width=30, textvariable=msgvar)
        self.output_ent.grid(row=2, column=0, sticky='e')

        #common feature label configuration
        self.lbl_opts = dict(width=6, anchor='w', font='Consolas 10')
        
        #annotation conversion
        self.conversion = {
            "<class 'int'>"  :lambda: tk.IntVar(),
            "<class 'str'>"  :lambda: tk.StringVar(),
            "<class 'bool'>" :lambda: tk.BooleanVar(),
            "<class 'float'>":lambda: tk.DoubleVar(),
        }

        #build a feature for every "feat_" suffixed method
        for feature in [func for func in dir(self) if callable(getattr(self, func)) and func.split('_')[0] in prefix]:
            self._add_feature(feature)

    #create a list of variable values
    def __tovalue(self, vars) -> List[int]:
        return [v.get() for v in vars]
        
    #dynamically create the gui for a method
    def _add_feature(self, feature):
        main = tk.Frame(self)
        main.grid(sticky='we')
        
        #parse feature components
        command = getattr(self, feature)
        featcmp = feature.split('_')
        
        if featcmp and len(featcmp) == 3:
            _, label, action = featcmp
            
            #create a list of Vars based on command argument types
            args, vars = inspect.signature(command).parameters, []
            for name in args:
                try:
                    #convert annotations to the proper tk.[Type]Var
                    vars.append(self.conversion[str(args[name].annotation)]())
                except KeyError:
                    #fallback to StringVar
                    vars.append(tk.StringVar())
    
            #create label and button for this command
            tk.Label(main, text=label, **self.lbl_opts).grid(row=0, column=0, sticky='e')
            tk.Button(main, text=action, width=5, command=lambda v=vars: command(*self.__tovalue(v))).grid(row=0, column=1, sticky='w', padx=8)
    
            #create an Entry for every argument in command
            for i, v in enumerate(vars):
                tk.Entry(main, width=2, textvariable=v).grid(row=0, column=i+2, sticky='w')
    
            #give all the weight to the last row
            main.grid_columnconfigure(i+2, weight=1)
        else:
            #feature name components did not pass expectations
            raise ValueError('ValueError: feature component must consist of three underscore-seperated parts as: PREFIX_LABEL_ACTION')


##EXAMPLES OF THE ULTIMATE IMPLEMENTATION ALL OF THE ABOVE ALLOWS


#generate GenericBoard display
class tkGenericBoard(tkBaseBoard, GenericBoard):
    def __init__(self, master, **kwargs):
        GenericBoard.__init__(self)
        tkBaseBoard.__init__(self, master, **self.attributes, **kwargs)


#generate LEDBoard display
class tkLEDBoard(tkBaseBoard, LEDBoard):
    def __init__(self, master, **kwargs):
        LEDBoard.__init__(self)
        tkBaseBoard.__init__(self, master, **self.attributes, **kwargs)


##EXAMPLE BASE USAGE


if __name__ == '__main__':
    root = tk.Tk()
    root.title('Example')
    root.configure(padx=2, pady=2)
    
    tkGenericBoard(root).grid()
    
    tkLEDBoard(root).grid()
    
    root.mainloop()