def construct(self, frame: ttk.Frame) -> None: self.data.setdefault("text", tk.StringVar(value="0.0")) self.label = ttk.Label(frame, text=self.name) self.entry = ValEntry(ValEntry.type_validator(float), frame, textvariable=self.data["text"]) self.entry.bind("<Return>", lambda _: self._callback("enter")) self.label.pack(side="left") self.entry.pack(side="left")
class FBFloatEntry(FBWidget): def construct(self, frame: ttk.Frame) -> None: self.data.setdefault("text", tk.StringVar(value="0.0")) self.label = ttk.Label(frame, text=self.name) self.entry = ValEntry(ValEntry.type_validator(float), frame, textvariable=self.data["text"]) self.entry.bind("<Return>", lambda _: self._callback("enter")) self.label.pack(side="left") self.entry.pack(side="left") def applyConfig(self): self.entry["width"] = int(self.config.setdefault("宽度", "20")) def rename(self, newName: str) -> None: self.label["text"] = newName
def constructWithBorder(self, frame: ttk.Frame) -> None: self.data.setdefault("text", tk.StringVar(value="0.0")) topFrame = ttk.Frame(frame) self.label = ttk.Label(topFrame, text=self.name) self.entry = ValEntry(ValEntry.type_validator(float), topFrame, textvariable=self.data["text"]) self.scale = ttk.Scale(frame, orient="horizontal", from_=0.0, to=MAXVAL, command=self._onScaleChange) rangeFrame = ttk.Frame(frame) self.lowLabel = ttk.Label(rangeFrame) self.highLabel = ttk.Label(rangeFrame) self.entry.bind("<Return>", lambda _: self._callback("enter")) self.entry.bindUpdate(self._calcScale) self.entry.bindUpdate(lambda _: self._callback("change")) self.scale.bind("<ButtonRelease-1>", lambda _: self._callback("release")) rangeFrame.bind("<MouseWheel>", self._onMouseWheel) self.scale.bind("<MouseWheel>", self._onMouseWheel) topFrame.pack() self.label.pack(side="left") self.entry.pack(side="left") self.scale.pack(fill="x", expand=True) rangeFrame.pack(fill="x", expand=True) self.lowLabel.pack(side="left") self.highLabel.pack(side="right")
class FBScaleEntry(FBWidget): def constructWithBorder(self, frame: ttk.Frame) -> None: self.data.setdefault("text", tk.StringVar(value="0.0")) topFrame = ttk.Frame(frame) self.label = ttk.Label(topFrame, text=self.name) self.entry = ValEntry(ValEntry.type_validator(float), topFrame, textvariable=self.data["text"]) self.scale = ttk.Scale(frame, orient="horizontal", from_=0.0, to=MAXVAL, command=self._onScaleChange) rangeFrame = ttk.Frame(frame) self.lowLabel = ttk.Label(rangeFrame) self.highLabel = ttk.Label(rangeFrame) self.entry.bind("<Return>", lambda _: self._callback("enter")) self.entry.bindUpdate(self._calcScale) self.entry.bindUpdate(lambda _: self._callback("change")) self.scale.bind("<ButtonRelease-1>", lambda _: self._callback("release")) rangeFrame.bind("<MouseWheel>", self._onMouseWheel) self.scale.bind("<MouseWheel>", self._onMouseWheel) topFrame.pack() self.label.pack(side="left") self.entry.pack(side="left") self.scale.pack(fill="x", expand=True) rangeFrame.pack(fill="x", expand=True) self.lowLabel.pack(side="left") self.highLabel.pack(side="right") def _onMouseWheel(self, event): if self.editing(): return if event.num == 5 or event.delta == -120: delta = -self.delta elif event.num == 4 or event.delta == 120: delta = self.delta else: return self.scale.set(max(0, min(MAXVAL, self.scale.get() + delta))) self._onScaleChange(self.scale.get()) def _calcScale(self, *_): value = float(self.entry.get()) self.scale.configure(value=max( 0.0, min(MAXVAL, (value - self.low) / (self.high - self.low) * MAXVAL))) def _onScaleChange(self, scale): scale = round(float(scale)) value = f"%.{self.displayPrecision}f" % ( self.low + (self.high - self.low) * scale / MAXVAL) if value != self.entry.get(): self.entry.set(value) self._callback("change") def applyConfig(self): self.entry["width"] = int(self.config.setdefault("宽度", "20")) self.low = float(self.config.setdefault("最小值", "0.0")) self.high = float(self.config.setdefault("最大值", "100.0")) self.displayPrecision = int(self.config.setdefault("显示精度", "2")) self.lowLabel["text"] = self.config["最小值"] self.highLabel["text"] = self.config["最大值"] self._calcScale() delta = float(self.config.setdefault("滚轮增量", "1.0")) self.delta = MAXVAL / (self.high - self.low) * delta def rename(self, newName: str) -> None: self.label["text"] = newName
def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBImg") self._root.rowconfigure(0, weight=1) self._root.columnconfigure(0, weight=1) self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._imgCanvas = ttk.Canvas(self._root) self._imgCanvas.config(bg="light gray") self._imgCanvas.grid(row=0, column=0, sticky="nsew") self._imgCanvas_shape = (0, 0) opFrame = ttk.Frame(self._root) opFrame.grid(row=1, column=0, sticky="we") opFrame.columnconfigure(0, weight=1) val = lambda s: ValEntry.type_validator(int)(s) and int(s) > 0 imgFrame = ttk.Frame(opFrame) imgFrame.grid(row=0, column=0, sticky="we") ttk.Label(imgFrame, text="宽度").pack(side="left", pady=5) self.wEntry = ValEntry(val, imgFrame, width=5) self.wEntry.pack(side="left", pady=5) ttk.Label(imgFrame, text="高度").pack(side="left", pady=5) self.hEntry = ValEntry(val, imgFrame, width=5) self.hEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="应用", command=self._applySize).pack(side="left", padx=2, pady=5) self._pauseButton = ttk.Checkbutton(imgFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._pauseButton.pack(side="left", padx=5, pady=5) ttk.Label(imgFrame, text="保存路径").pack(side="left", pady=5) self._dirEntry = ttk.Entry(imgFrame, width=30) self._dirEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="选择", command=self._selectDir).pack(side="left", pady=5) self._saveButton = ttk.Button(imgFrame, text="保存", bootstyle=("success"), command=self.saveImg) self._saveButton.pack(side="left", padx=5, pady=5) self._recordButton = ttk.Checkbutton(imgFrame, text="录制", command=self._toggleRecord, bootstyle=("warning", "outline", "toolbutton")) self._recordButton.pack(side="left", pady=5) self._cntLabel = ttk.Label(imgFrame) self._cntLabel.pack(side="left", pady=5) self._client = FBServer() if isServer else FBClient() self._recv = FBRawRecv() self._client.registerRecvCallback(self._recv.input) self._recv.registerRecvCallback(self.updateData) self._imgLock = threading.Lock() self._dirLock = threading.Lock() self.loadConfig()
class FBImgApp: def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBImg") self._root.rowconfigure(0, weight=1) self._root.columnconfigure(0, weight=1) self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._imgCanvas = ttk.Canvas(self._root) self._imgCanvas.config(bg="light gray") self._imgCanvas.grid(row=0, column=0, sticky="nsew") self._imgCanvas_shape = (0, 0) opFrame = ttk.Frame(self._root) opFrame.grid(row=1, column=0, sticky="we") opFrame.columnconfigure(0, weight=1) val = lambda s: ValEntry.type_validator(int)(s) and int(s) > 0 imgFrame = ttk.Frame(opFrame) imgFrame.grid(row=0, column=0, sticky="we") ttk.Label(imgFrame, text="宽度").pack(side="left", pady=5) self.wEntry = ValEntry(val, imgFrame, width=5) self.wEntry.pack(side="left", pady=5) ttk.Label(imgFrame, text="高度").pack(side="left", pady=5) self.hEntry = ValEntry(val, imgFrame, width=5) self.hEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="应用", command=self._applySize).pack(side="left", padx=2, pady=5) self._pauseButton = ttk.Checkbutton(imgFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._pauseButton.pack(side="left", padx=5, pady=5) ttk.Label(imgFrame, text="保存路径").pack(side="left", pady=5) self._dirEntry = ttk.Entry(imgFrame, width=30) self._dirEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="选择", command=self._selectDir).pack(side="left", pady=5) self._saveButton = ttk.Button(imgFrame, text="保存", bootstyle=("success"), command=self.saveImg) self._saveButton.pack(side="left", padx=5, pady=5) self._recordButton = ttk.Checkbutton(imgFrame, text="录制", command=self._toggleRecord, bootstyle=("warning", "outline", "toolbutton")) self._recordButton.pack(side="left", pady=5) self._cntLabel = ttk.Label(imgFrame) self._cntLabel.pack(side="left", pady=5) self._client = FBServer() if isServer else FBClient() self._recv = FBRawRecv() self._client.registerRecvCallback(self._recv.input) self._recv.registerRecvCallback(self.updateData) self._imgLock = threading.Lock() self._dirLock = threading.Lock() self.loadConfig() def _toggleRecord(self, *_): if self._recordButton.instate(["selected"]): self._saveButton.config(state="disabled") self._recordButton.config(text="停止") else: self._saveButton.config(state="!disabled") self._recordButton.config(text="录制") def _updateDir(self): os.makedirs(self.save_dir, exist_ok=True) self._dirEntry.config(state="normal") self._dirEntry.delete(0, tk.END) self._dirEntry.insert(0, self.save_dir) self._dirEntry.config(state="readonly") def _selectDir(self, *_): with self._dirLock: cur = self.save_dir s = filedialog.askdirectory(master=self._root, initialdir=cur, title="选择保存路径", mustexist=False) if not s: return with self._dirLock: self.save_dir = s self._updateDir() def saveImg(self, *_): def save(): with self._imgLock: img = self.img with self._dirLock: Dir = self.save_dir name = datetime.datetime.now().strftime( "%Y-%m-%d-%H-%M-%S-%f") + SAVE_EXT cv.imwrite(os.path.join(Dir, name), img) threading.Thread(target=save).start() def updateData(self, data: bytes) -> None: if self._pauseButton.instate(["selected"]): return with self._imgLock: w, h = self.w, self.h img = np.frombuffer(data, dtype=np.uint8).reshape((h, w)) with self._imgLock: if w != self.w or h != self.h: return self.img = img self._updateFlag = True if self._recordButton.instate(["selected"]): self.saveImg() def _applySize(self): with self._imgLock: self.w, self.h = int(self.wEntry.get()), int(self.hEntry.get()) self.img = np.zeros((self.h, self.w), dtype=np.uint8) self._updateFlag = True self._recv.setConfig(cnt=self.w * self.h) def loadConfig(self): os.makedirs(CFG_DIR, exist_ok=True) cfg = {} if os.path.exists(CFG_PATH): with open(CFG_PATH, "r") as f: cfg = json.load(f) self._root.geometry(cfg.get("geometry", "876x600+30+30")) self.w = cfg.get("w", 752) self.h = cfg.get("h", 480) self.wEntry.set(str(self.w)) self.hEntry.set(str(self.h)) self.save_dir = cfg.get("save_dir", SAVE_DIR) self._updateDir() self._applySize() def saveConfig(self): cfg = { "geometry": self._root.geometry(), "w": self.w, "h": self.h, "save_dir": self.save_dir } with open(CFG_PATH, "w") as f: json.dump(cfg, f) def create_image(self): canvas_w, canvas_h = self._imgCanvas.winfo_width( ), self._imgCanvas.winfo_height() with self._imgLock: if (canvas_w, canvas_h ) == self._imgCanvas_shape and not self._updateFlag: return img = self.img self._updateFlag = False self._imgCanvas_shape = (canvas_w, canvas_h) img_w, img_h = img.shape[1], img.shape[0] canvas_k, img_k = canvas_w / canvas_h, img_w / img_h w, h = (canvas_w, round(canvas_w / img_k)) if canvas_k < img_k else (round(canvas_h * img_k), canvas_h) resized_img = cv.resize(img, (max(1, w), max(1, h)), interpolation=cv.INTER_NEAREST) cvt_img = resized_img self._imgTK = ImageTk.PhotoImage(master=self._imgCanvas, image=Image.fromarray(cvt_img)) self._imgCanvas.create_image(canvas_w >> 1, canvas_h >> 1, image=self._imgTK) def _create_image_task(self): if self._running: self.create_image() if self._running: self._imgCanvas.after(50, self._create_image_task) def mainloop(self): self._running = True self._updateFlag = True self._create_image_task() self._client.start() self._recv.start() self._root.mainloop() def shutdown(self): self._running = False self._recv.shutdown() self._client.shutdown() self.saveConfig() self._root.destroy()
def __init__(self, master=None, is_vertical=True, queue_size: int = 10, **kw): super().__init__(master, **kw) self._recv = FBFloatRecv(queue_size=queue_size) self.input = self._recv.input self.registerRecvCallback = self._recv.registerRecvCallback self.start = self._recv.start idLabel = ttk.Label(self, text="ID") self._idCbo = ttk.Combobox(self, width=4, state="readonly", values=["无"] + list(range(256))) self._idCbo.current(0) self._idCbo.bind("<<ComboboxSelected>>", self._applyID) cntLabel = ttk.Label(self, text="个数") self._cntEntry = ValEntry(lambda s: s.isdigit() and int(s) > 0, self, width=5) bitsLabel = ttk.Label(self, text="位数") self._bitsCbo = ttk.Combobox(self, width=4, state="readonly", values=[4, 8]) self._bitsCbo.current(0) checkLabel = ttk.Label(self, text="校验") self._checkCbo = ttk.Combobox(self, width=4, state="readonly", values=["sum", "none"]) self._checkCbo.current(0) extra = self.extraBody(self) self._cbs: callable = [] applyButton = ttk.Button(self, text="应用", command=self._clickCB) if is_vertical: idLabel.grid(row=0, column=0, sticky="w") self._idCbo.grid(row=0, column=1, sticky="w") cntLabel.grid(row=1, column=0, sticky="w") self._cntEntry.grid(row=1, column=1, sticky="we") bitsLabel.grid(row=2, column=0, sticky="w") self._bitsCbo.grid(row=2, column=1, sticky="we") checkLabel.grid(row=3, column=0, sticky="w") self._checkCbo.grid(row=3, column=1, sticky="we") if extra is not None: extra.grid(row=4, column=0, columnspan=2, sticky="we") applyButton.grid(row=4 + (extra is not None), column=0, columnspan=2, sticky="we") else: idLabel.pack(side="left") self._idCbo.pack(side="left", padx=2) cntLabel.pack(side="left") self._cntEntry.pack(side="left", padx=2) bitsLabel.pack(side="left") self._bitsCbo.pack(side="left", padx=2) checkLabel.pack(side="left") self._checkCbo.pack(side="left", padx=2) if extra is not None: extra.pack(side="left") applyButton.pack(side="left", padx=2) self.loadConfig()
class FBFloatRecvGUI(ttk.Frame): def __init__(self, master=None, is_vertical=True, queue_size: int = 10, **kw): super().__init__(master, **kw) self._recv = FBFloatRecv(queue_size=queue_size) self.input = self._recv.input self.registerRecvCallback = self._recv.registerRecvCallback self.start = self._recv.start idLabel = ttk.Label(self, text="ID") self._idCbo = ttk.Combobox(self, width=4, state="readonly", values=["无"] + list(range(256))) self._idCbo.current(0) self._idCbo.bind("<<ComboboxSelected>>", self._applyID) cntLabel = ttk.Label(self, text="个数") self._cntEntry = ValEntry(lambda s: s.isdigit() and int(s) > 0, self, width=5) bitsLabel = ttk.Label(self, text="位数") self._bitsCbo = ttk.Combobox(self, width=4, state="readonly", values=[4, 8]) self._bitsCbo.current(0) checkLabel = ttk.Label(self, text="校验") self._checkCbo = ttk.Combobox(self, width=4, state="readonly", values=["sum", "none"]) self._checkCbo.current(0) extra = self.extraBody(self) self._cbs: callable = [] applyButton = ttk.Button(self, text="应用", command=self._clickCB) if is_vertical: idLabel.grid(row=0, column=0, sticky="w") self._idCbo.grid(row=0, column=1, sticky="w") cntLabel.grid(row=1, column=0, sticky="w") self._cntEntry.grid(row=1, column=1, sticky="we") bitsLabel.grid(row=2, column=0, sticky="w") self._bitsCbo.grid(row=2, column=1, sticky="we") checkLabel.grid(row=3, column=0, sticky="w") self._checkCbo.grid(row=3, column=1, sticky="we") if extra is not None: extra.grid(row=4, column=0, columnspan=2, sticky="we") applyButton.grid(row=4 + (extra is not None), column=0, columnspan=2, sticky="we") else: idLabel.pack(side="left") self._idCbo.pack(side="left", padx=2) cntLabel.pack(side="left") self._cntEntry.pack(side="left", padx=2) bitsLabel.pack(side="left") self._bitsCbo.pack(side="left", padx=2) checkLabel.pack(side="left") self._checkCbo.pack(side="left", padx=2) if extra is not None: extra.pack(side="left") applyButton.pack(side="left", padx=2) self.loadConfig() def extraBody(self, master: ttk.Frame) -> ttk.Frame: return None def registerClickCallback(self, func: callable): self._cbs.append(func) def _clickCB(self): self._applyConfig() for cb in self._cbs: cb() def getID(self): return self._idCbo.current() - 1 def getCnt(self): return int(self._cntEntry.get()) def _applyConfig(self): id = self.getID() self.cfg["id"] = str(id) cfg = self.cfg["cfg"][str(id)] cnt = cfg["cnt"] = self.getCnt() bits = cfg["bits"] = int(self._bitsCbo.get()) check = cfg["check"] = self._checkCbo.get() self._recv.setConfig(id=id, cnt=cnt, bits=bits, checksum=check == "sum") @staticmethod def _getDefualtCfg(): return {"cnt": 1, "bits": 4, "check": "sum"} def _applyID(self, *_): cfg = self.cfg["cfg"].setdefault(str(self.getID()), self._getDefualtCfg()) self._cntEntry.set(str(cfg["cnt"])) self._bitsCbo.current(0 if cfg["bits"] == 4 else 1) self._checkCbo.current(0 if cfg["check"] == "sum" else 1) def loadConfig(self): os.makedirs(CFG_DIR, exist_ok=True) self.cfg = {} if os.path.exists(CFG_PATH): with open(CFG_PATH, "r") as f: self.cfg = json.load(f) id = self.cfg.setdefault("id", "-1") self._idCbo.current(int(id) + 1) self.cfg.setdefault("cfg", {}) self._applyID() self._applyConfig() def saveConfig(self): res = {"id": self.cfg["id"], "cfg": {}} defaultCfg = self._getDefualtCfg() for id, cfg in self.cfg["cfg"].items(): if cfg != defaultCfg: res["cfg"][id] = cfg with open(CFG_PATH, "w") as f: json.dump(res, f) def shutdown(self): self.saveConfig() self._recv.shutdown()