From 44c582487e470405e38ed604e9a6dd6e4fcf96a7 Mon Sep 17 00:00:00 2001 From: risingLee <871066422@qq.com> Date: Sat, 7 Feb 2026 01:57:05 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=86=E8=8A=82=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard.json | 63 +++++++++++++++++++++ influx_settings.json | 2 +- main.py | 129 ++++++++++++++++++++++++++++++------------- 3 files changed, 156 insertions(+), 38 deletions(-) create mode 100644 dashboard.json diff --git a/dashboard.json b/dashboard.json new file mode 100644 index 0000000..f729764 --- /dev/null +++ b/dashboard.json @@ -0,0 +1,63 @@ +{ + "canvas": { + "x": 0.0, + "y": 0.0, + "w": 1920.0, + "h": 1080.0 + }, + "widgets": [ + { + "widget_type": "label", + "x": 173.0, + "y": 210.0, + "w": 162.0, + "h": 62.0, + "z": 1.0, + "config": { + "fieldName": "主轴承#3", + "prefix": "主轴承温度", + "suffix": " ℃", + "fontSize": 16, + "color": "#FFFFFF" + } + }, + { + "widget_type": "label", + "x": 46.0, + "y": 378.0, + "w": 162.0, + "h": 62.0, + "z": 1.0, + "config": { + "fieldName": "减速箱小轴承2", + "prefix": "减速箱小轴承温度", + "suffix": "℃", + "fontSize": 16, + "color": "#FFFFFF" + } + }, + { + "widget_type": "image", + "x": 0.0, + "y": 0.0, + "w": 650.0, + "h": 865.0, + "z": 0.0, + "config": { + "imagePath": "D:/1-2.JPG" + } + }, + { + "widget_type": "web", + "x": 648.5, + "y": 0.0, + "w": 887.0, + "h": 863.0, + "z": 0.0, + "config": { + "url": "http://127.0.0.1:8086/orgs/b2542eeb72a3e614/dashboards/0f8ab8a328fe9000?lower=now%28%29+-+1h", + "locked": false + } + } + ] +} \ No newline at end of file diff --git a/influx_settings.json b/influx_settings.json index f6aa709..fcd831d 100644 --- a/influx_settings.json +++ b/influx_settings.json @@ -4,5 +4,5 @@ "org": "MEASCON", "bucket": "PCM", "interval_ms": 1000, - "query": "from(bucket: \"PCM\")\n |> range(start: -3h)\n |> filter(fn: (r) => r._measurement == \"PCM_Measurement\")\n |> filter(fn: (r) => r.data_type == \"LSDAQ\")\n |> keep(columns: [\"_time\", \"_field\", \"_value\"])\n |> last()" + "query": "from(bucket: \"PCM\")\n |> range(start: -24h)\n |> filter(fn: (r) => r._measurement == \"PCM_Measurement\")\n |> filter(fn: (r) => r.data_type == \"LSDAQ\")\n |> keep(columns: [\"_time\", \"_field\", \"_value\"])\n |> last()" } \ No newline at end of file diff --git a/main.py b/main.py index 1e8d719..85a3dd0 100644 --- a/main.py +++ b/main.py @@ -201,6 +201,25 @@ class DashboardItem(QGraphicsRectItem): super().paint(painter, option, widget) def to_state(self) -> WidgetState: + """将组件状态转换为 WidgetState,使用精确的 pos() 和 rect(),而不是 sceneBoundingRect()""" + pos = self.pos() + r = self.rect() + return WidgetState( + widget_type=self._get_widget_type(), + x=float(pos.x()), + y=float(pos.y()), + w=float(r.width()), + h=float(r.height()), + z=float(self.zValue()), + config=self._get_config(), + ) + + def _get_widget_type(self) -> str: + """子类需要重写,返回组件类型""" + raise NotImplementedError + + def _get_config(self) -> Dict[str, Any]: + """子类需要重写,返回配置字典""" raise NotImplementedError def apply_state(self, state: WidgetState): @@ -387,17 +406,11 @@ class ImageItem(DashboardItem): ) self._pixmap_item.setScale(scale) - def to_state(self) -> WidgetState: - r = self.sceneBoundingRect() - return WidgetState( - widget_type="image", - x=r.x(), - y=r.y(), - w=r.width(), - h=r.height(), - z=float(self.zValue()), - config={"imagePath": self._image_path}, - ) + def _get_widget_type(self) -> str: + return "image" + + def _get_config(self) -> Dict[str, Any]: + return {"imagePath": self._image_path} def apply_state(self, state: WidgetState): super().apply_state(state) @@ -484,17 +497,11 @@ class LabelItem(DashboardItem): if self._field_name and self._field_name in data: self._update_preview_text(data[self._field_name]) - def to_state(self) -> WidgetState: - r = self.sceneBoundingRect() - return WidgetState( - widget_type="label", - x=r.x(), - y=r.y(), - w=r.width(), - h=r.height(), - z=float(self.zValue()), - config=self.label_config(), - ) + def _get_widget_type(self) -> str: + return "label" + + def _get_config(self) -> Dict[str, Any]: + return self.label_config() def apply_state(self, state: WidgetState): super().apply_state(state) @@ -558,20 +565,14 @@ class WebItem(DashboardItem): def is_locked(self) -> bool: return self._locked - def to_state(self) -> WidgetState: - r = self.sceneBoundingRect() - return WidgetState( - widget_type="web", - x=r.x(), - y=r.y(), - w=r.width(), - h=r.height(), - z=float(self.zValue()), - config={ - "url": self._url, - "locked": self._locked, - }, - ) + def _get_widget_type(self) -> str: + return "web" + + def _get_config(self) -> Dict[str, Any]: + return { + "url": self._url, + "locked": self._locked, + } def apply_state(self, state: WidgetState): super().apply_state(state) @@ -715,9 +716,35 @@ class DashboardView(QGraphicsView): # 布局持久化 def save_layout(self, path: str): states = [] + canvas_bounds = self._scene.canvas_rect() + canvas_pos = self._scene.canvas_item.pos() + canvas_left = canvas_pos.x() + canvas_top = canvas_pos.y() + canvas_right = canvas_left + canvas_bounds.width() + canvas_bottom = canvas_top + canvas_bounds.height() + for it in self._scene.items(): if isinstance(it, DashboardItem): - states.append(asdict(it.to_state())) + # 获取组件状态 + state = it.to_state() + state_dict = asdict(state) + + # 修正超出边界的位置数据(直接修改字典,不移动组件) + item_left = state_dict["x"] + item_top = state_dict["y"] + item_w = state_dict["w"] + item_h = state_dict["h"] + + # 修正超出边界的位置数据 + final_x = max(canvas_left, min(item_left, canvas_right - item_w)) + final_y = max(canvas_top, min(item_top, canvas_bottom - item_h)) + + # 更新状态数据 + state_dict["x"] = final_x + state_dict["y"] = final_y + + states.append(state_dict) + with open(path, "w", encoding="utf-8") as f: payload = { "canvas": self._scene.canvas_state(), @@ -769,6 +796,28 @@ class DashboardView(QGraphicsView): else: continue it.apply_state(ws) + + # 加载后也应用边界限制,确保组件不超出画布 + canvas_bounds = self._scene.canvas_rect() + canvas_pos = self._scene.canvas_item.pos() + canvas_left = canvas_pos.x() + canvas_top = canvas_pos.y() + canvas_right = canvas_left + canvas_bounds.width() + canvas_bottom = canvas_top + canvas_bounds.height() + + pos = it.pos() + r = it.rect() + item_left = pos.x() + item_top = pos.y() + item_right = item_left + r.width() + item_bottom = item_top + r.height() + + # 修正超出边界的位置 + final_x = max(canvas_left, min(item_left, canvas_right - r.width())) + final_y = max(canvas_top, min(item_top, canvas_bottom - r.height())) + + if final_x != item_left or final_y != item_top: + it.setPos(final_x, final_y) # ----------------------------- @@ -1106,6 +1155,8 @@ class MainWindow(QMainWindow): super().__init__() self.setWindowTitle("PCM Viewer - Widgets Dashboard") self.resize(1400, 840) + # 设置焦点策略,确保能够接收键盘事件(特别是 ESC 键) + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self._edit_mode = True # 全屏编辑标志(展示模式本身也会全屏) self._fullscreen_edit = False @@ -1219,12 +1270,16 @@ class MainWindow(QMainWindow): if want_full: # 真全屏:使用 Qt 的 showFullScreen,退出时用 showMaximized 恢复普通 Windows 窗口 self.showFullScreen() + # 全屏后确保窗口获得焦点,以便接收键盘事件(ESC 键) + self.setFocus() # 全屏时关闭滚动条,避免 1920x1080 屏幕上还出现滚动条 self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) else: # 恢复为标准的最大化窗口(带边框/任务栏) self.showMaximized() + # 恢复焦点,确保能够接收键盘事件 + self.setFocus() self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)