panel,dialog,window组件越界问题汇总

本文提供了一种解决EasyUI中panel, window, dialog组件在拖拽或调整大小时越界的方法,通过自定义onResize和onMove事件,确保组件不会超出其父元素范围。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前分别写过panel,dialog,window三个组件因为拖曳或者reSize造成组件越界而无法还原的问题,两篇文章分别针对拖曳和reSize给出了解决方案。不过根据朋友的反馈,reSize的解决方案拖曳的解决方案同时使用时存在效率低下的问题,个人也在进一步使用过程中发现了另外一些问题,共修正以下Bug:

  • 原生panel并无拖曳和缩放功能,且继承panel组件的上层组件太多,极容易出问题,故放弃对panel组件的支持。
  • onResize配合onMove使用时,性能低下,原因是由onResize触发的onMove内部死循环。已修正。
  • resize时,超越浏览器边界会造成缩放和拖动都不可用。通过增加了对offset的监控修正
  • IE8下,reSize超越浏览器边界后依旧会造成缩放和拖曳不可用,原因是IE8此时不影响onkeyup事件。已修正。
  • window,dioalg内部包含layout,datagrid组件时,resize高度小于一定值会造成性能低下。已修正。
  • 初始化时,如果页面不是最大化,onResize会把window和dialog高度自动变小。通过计数器修正。
实现代码:

最终综合两种方案,整理出代码:

 
  1. var ie = (function() {   
  2.     var undef, v = 3, div = document.createElement('div'), all = div   
  3.             .getElementsByTagName('i');   
  4.     while (div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->', all[0]);   
  5.     return v > 4 ? v : undef;   
  6. }());   
  7. /**
  8.  * add by cgh  
  9.  * 针对panel window dialog三个组件调整大小时会超出父级元素的修正  
  10.  * 如果父级元素的overflow属性为hidden,则修复上下左右个方向  
  11.  * 如果父级元素的overflow属性为非hidden,则只修复上左两个方向  
  12.  * @param width  
  13.  * @param height  
  14.  * @returns  
  15.  */  
  16. var easyuiPanelOnResize = function(width, height) {   
  17.     if (!$.data(this, 'window') && !$.data(this, 'dialog'))   
  18.         return;   
  19.   
  20.     if (ie === 8) {   
  21.         var data = $.data(this, "window") || $.data(this, "dialog");   
  22.         if (data.pmask) {   
  23.             var masks = data.window.nextAll('.window-proxy-mask');   
  24.             if (masks.length > 1) {   
  25.                 $(masks[1]).remove();   
  26.                 masks[1] = null;   
  27.             }   
  28.         }   
  29.     }   
  30.     if ($(this).panel('options').maximized == true) {   
  31.         $(this).panel('options').fit = false;   
  32.     }   
  33.     $(this).panel('options').reSizing = true;   
  34.     if (!$(this).panel('options').reSizeNum) {   
  35.         $(this).panel('options').reSizeNum = 1;   
  36.     } else {   
  37.         $(this).panel('options').reSizeNum++;   
  38.     }   
  39.     var parentObj = $(this).panel('panel').parent();   
  40.     var left = $(this).panel('panel').position().left;   
  41.     var top = $(this).panel('panel').position().top;   
  42.   
  43.     if ($(this).panel('panel').offset().left < 0) {   
  44.         $(this).panel('move', {   
  45.                     left : 0   
  46.                 });   
  47.     }   
  48.     if ($(this).panel('panel').offset().top < 0) {   
  49.         $(this).panel('move', {   
  50.                     top : 0   
  51.                 });   
  52.     }   
  53.   
  54.     if (left < 0) {   
  55.         $(this).panel('move', {   
  56.                     left : 0   
  57.                 }).panel('resize', {   
  58.                     width : width + left   
  59.                 });   
  60.     }   
  61.     if (top < 0) {   
  62.         $(this).panel('move', {   
  63.                     top : 0   
  64.                 }).panel('resize', {   
  65.                     height : height + top   
  66.                 });   
  67.     }   
  68.     if (parentObj.css("overflow") == "hidden") {   
  69.         var inline = $.data(this, "window").options.inline;   
  70.         if (inline == false) {   
  71.             parentObj = $(window);   
  72.         }   
  73.   
  74.         if ((width + left > parentObj.width())   
  75.                 && $(this).panel('options').reSizeNum > 1) {   
  76.             $(this).panel('resize', {   
  77.                         width : parentObj.width() - left   
  78.                     });   
  79.         }   
  80.   
  81.         if ((height + top > parentObj.height())   
  82.                 && $(this).panel('options').reSizeNum > 1) {   
  83.             $(this).panel('resize', {   
  84.                         height : parentObj.height() - top   
  85.                     });   
  86.         }   
  87.     }   
  88.     $(this).panel('options').reSizing = false;   
  89. };   
  90. /**
  91.  * add by cgh  
  92.  * 针对panel window dialog三个组件拖动时会超出父级元素的修正  
  93.  * 如果父级元素的overflow属性为hidden,则修复上下左右个方向  
  94.  * 如果父级元素的overflow属性为非hidden,则只修复上左两个方向  
  95.  * @param left  
  96.  * @param top  
  97.  * @returns  
  98.  */  
  99. var easyuiPanelOnMove = function(left, top) {   
  100.     if ($(this).panel('options').reSizing)   
  101.         return;   
  102.     var parentObj = $(this).panel('panel').parent();   
  103.     var width = $(this).panel('options').width;   
  104.     var height = $(this).panel('options').height;   
  105.     var right = left + width;   
  106.     var buttom = top + height;   
  107.     var parentWidth = parentObj.width();   
  108.     var parentHeight = parentObj.height();   
  109.   
  110.     if (left < 0) {   
  111.         $(this).panel('move', {   
  112.                     left : 0   
  113.                 });   
  114.     }   
  115.     if (top < 0) {   
  116.         $(this).panel('move', {   
  117.                     top : 0   
  118.                 });   
  119.     }   
  120.   
  121.     if (parentObj.css("overflow") == "hidden") {   
  122.         var inline = $.data(this, "window").options.inline;   
  123.         if (inline == false) {   
  124.             parentObj = $(window);   
  125.         }   
  126.         if (left > parentObj.width() - width) {   
  127.             $(this).panel('move', {   
  128.                         "left" : parentObj.width() - width   
  129.                     });   
  130.         }   
  131.         if (top > parentObj.height() - height) {   
  132.             $(this).panel('move', {   
  133.                         "top" : parentObj.height() - height   
  134.                     });   
  135.         }   
  136.     }   
  137. };   
  138.   
  139. $.fn.window.defaults.onResize = easyuiPanelOnResize;   
  140. $.fn.dialog.defaults.onResize = easyuiPanelOnResize;   
  141. $.fn.window.defaults.onMove = easyuiPanelOnMove;   
  142. $.fn.dialog.defaults.onMove = easyuiPanelOnMove;  

使用的时候,请在引入easyui的核心文件后,直接追加以上代码,注意不要写在document.ready里面。

到这里,panel,window,dialog等组件越界的问题就算是基本解决了。欢迎大家测试,即时反馈Bug。

效果演示:

http://www.easyui.info/easyui/demo/window/062.html

import sys import cv2 import numpy as np from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QDialog, QLabel, QPushButton, QFileDialog, QTextEdit, QTabWidget, QMessageBox, QProgressBar, QSlider, QComboBox, QGroupBox, QGridLayout, QToolBar, QStatusBar, QDockWidget, QSplitter, QScrollArea, QMenu, QSpinBox, QDoubleSpinBox, QCheckBox, QRadioButton, QButtonGroup # 添加缺失的导入 ) from PySide6.QtGui import ( QAction, QPixmap, QImage, QPainter, QPen, QColor, QIcon, QKeySequence, QTransform, QCursor ) from PySide6.QtCore import Qt, QThread, Signal, QPoint, QSize, QRect import matplotlib matplotlib.use('Agg') # 使用Agg后端,不显示图形窗口 import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import os import math from scipy import ndimage try: from skimage.feature import graycomatrix, graycoprops except ImportError: from skimage.feature import greycomatrix as graycomatrix, greycoprops as graycoprops plt.rcParams["font.family"] = ["SimHei"] # 仅保留 SimHei(黑体) plt.rcParams["axes.unicode_minus"] = False class ImageProcessingThread(QThread): """图像处理的工作线程,避免界面卡顿""" finished = Signal(object) def __init__(self, function, *args): super().__init__() self.function = function self.args = args def run(self): result = self.function(*self.args) self.finished.emit(result) class ImageViewer(QWidget): """图像显示组件,支持缩放和平移""" def __init__(self, parent=None): super().__init__(parent) self.image = QImage() self.scale_factor = 1.0 self.dragging = False self.last_pos = QPoint() self.setMouseTracking(True) layout = QVBoxLayout(self) self.label = QLabel(self) self.label.setAlignment(Qt.AlignCenter) self.label.setMinimumSize(1, 1) # 允许缩小 layout.addWidget(self.label) def set_image(self, image): self.image = image self.update_pixmap() def update_pixmap(self): if not self.image.isNull(): scaled_pixmap = QPixmap.fromImage(self.image).scaled( self.image.width() * self.scale_factor, self.image.height() * self.scale_factor, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.label.setPixmap(scaled_pixmap) def wheelEvent(self, event): """鼠标滚轮缩放""" delta = event.angleDelta().y() if delta > 0: self.scale_factor *= 1.1 else: self.scale_factor *= 0.9 self.update_pixmap() def mousePressEvent(self, event): """鼠标按下开始拖动""" if event.button() == Qt.LeftButton: self.dragging = True self.last_pos = event.position().toPoint() # 修改后 def mouseMoveEvent(self, event): """鼠标拖动图像""" if self.dragging: delta = event.pos() - self.last_pos scroll_bar = self.parent().horizontalScrollBar() scroll_bar.setValue(scroll_bar.value() - delta.x()) scroll_bar = self.parent().verticalScrollBar() scroll_bar.setValue(scroll_bar.value() - delta.y()) self.last_pos = event.pos() def mouseReleaseEvent(self, event): """鼠标释放结束拖动""" if event.button() == Qt.LeftButton: self.dragging = False def resizeEvent(self, event): """窗口大小变化时更新图像显示""" self.update_pixmap() super().resizeEvent(event) class HistogramWidget(QWidget): """直方图显示组件""" def __init__(self, parent=None): super().__init__(parent) self.figure = Figure(figsize=(5, 3), dpi=100) self.canvas = FigureCanvas(self.figure) layout = QVBoxLayout(self) layout.addWidget(self.canvas) self.axes = self.figure.add_subplot(111) def update_histogram(self, image): """更新直方图显示""" self.axes.clear() if image.ndim == 3: # 彩色图像 colors = ('b', 'g', 'r') for i, color in enumerate(colors): hist = cv2.calcHist([image], [i], None, [256], [0, 256]) self.axes.plot(hist, color=color) else: # 灰度图像 hist = cv2.calcHist([image], [0], None, [256], [0, 256]) self.axes.plot(hist, color='black') self.axes.set_xlim([0, 256]) self.axes.set_title('图像直方图') self.axes.set_xlabel('像素值') self.axes.set_ylabel('像素数量') self.figure.tight_layout() self.canvas.draw() class GLCMWidget(QWidget): """灰度共生矩阵(GLCM)特征显示组件""" def __init__(self, parent=None): super().__init__(parent) layout = QVBoxLayout(self) self.text_edit = QTextEdit() self.text_edit.setReadOnly(True) layout.addWidget(self.text_edit) def update_glcm(self, image): """更新GLCM特征显示""" if image.ndim == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 量化到16级灰度以减少计算量 image_quantized = (image // 16).astype(np.uint8) # 计算GLCM矩阵 (距离=1, 角度=0, 45, 90, 135度) glcm = graycomatrix(image_quantized, distances=[1], angles=[0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], levels=16, symmetric=True, normed=True) # 计算各种特征 contrast = graycoprops(glcm, 'contrast')[0] dissimilarity = graycoprops(glcm, 'dissimilarity')[0] homogeneity = graycoprops(glcm, 'homogeneity')[0] energy = graycoprops(glcm, 'energy')[0] correlation = graycoprops(glcm, 'correlation')[0] asm = graycoprops(glcm, 'ASM')[0] # 显示结果 result = "灰度共生矩阵(GLCM)特征:\n\n" result += f"对比度: {contrast}\n" result += f"相异性: {dissimilarity}\n" result += f"同质性: {homogeneity}\n" result += f"能量: {energy}\n" result += f"相关性: {correlation}\n" result += f"角二阶矩: {asm}\n" self.text_edit.setText(result) class ProcessingDialog(QDialog): def __init__(self, title, parent=None): super().__init__(parent) self.setWindowTitle(title) self.layout = QGridLayout(self) self.setLayout(self.layout) self.row = 0 def add_slider(self, label_text, min_val, max_val, default_val, step=1, callback=None): """添加滑块控件""" label = QLabel(label_text) slider = QSlider(Qt.Horizontal) slider.setMinimum(min_val) slider.setMaximum(max_val) slider.setValue(default_val) slider.setSingleStep(step) spinbox = QSpinBox() # 修正拼写错误 spinbox.setMinimum(min_val) spinbox.setMaximum(max_val) spinbox.setValue(default_val) spinbox.setSingleStep(step) # 同步滑块和数值框 slider.valueChanged.connect(spinbox.setValue) spinbox.valueChanged.connect(slider.setValue) if callback: slider.valueChanged.connect(callback) self.layout.addWidget(label, self.row, 0) self.layout.addWidget(slider, self.row, 1) self.layout.addWidget(spinbox, self.row, 2) self.row += 1 return slider, spinbox def add_double_slider(self, label_text, min_val, max_val, default_val, step=0.1, decimals=1, callback=None): """添加浮点数滑块控件""" label = QLabel(label_text) slider = QSlider(Qt.Horizontal) slider.setMinimum(int(min_val * 10)) slider.setMaximum(int(max_val * 10)) slider.setValue(int(default_val * 10)) spinbox = QDoubleSpinBox() spinbox.setMinimum(min_val) spinbox.setMaximum(max_val) spinbox.setValue(default_val) spinbox.setSingleStep(step) spinbox.setDecimals(decimals) # 同步滑块和数值框 def update_slider(value): slider.setValue(int(value * 10)) def update_spinbox(value): spinbox.setValue(value / 10) slider.valueChanged.connect(update_spinbox) spinbox.valueChanged.connect(update_slider) if callback: slider.valueChanged.connect(lambda: callback(spinbox.value())) self.layout.addWidget(label, self.row, 0) self.layout.addWidget(slider, self.row, 1) self.layout.addWidget(spinbox, self.row, 2) self.row += 1 return slider, spinbox def add_combo_box(self, label_text, items, default_index=0, callback=None): """添加下拉选择框""" label = QLabel(label_text) combo_box = QComboBox() combo_box.addItems(items) combo_box.setCurrentIndex(default_index) if callback: combo_box.currentIndexChanged.connect(callback) self.layout.addWidget(label, self.row, 0) self.layout.addWidget(combo_box, self.row, 1, 1, 2) self.row += 1 return combo_box def add_checkbox(self, label_text, default_state=False, callback=None): """添加复选框""" checkbox = QCheckBox(label_text) checkbox.setChecked(default_state) if callback: checkbox.stateChanged.connect(callback) self.layout.addWidget(checkbox, self.row, 0, 1, 3) self.row += 1 return checkbox def add_radio_buttons(self, label_text, options, default_index=0, callback=None): """添加单选按钮组""" label = QLabel(label_text) button_group = QButtonGroup(self) layout = QHBoxLayout() for i, option in enumerate(options): radio = QRadioButton(option) if i == default_index: radio.setChecked(True) button_group.addButton(radio, i) layout.addWidget(radio) if callback: button_group.buttonClicked.connect(callback) self.layout.addWidget(label, self.row, 0) self.layout.addLayout(layout, self.row, 1, 1, 2) self.row += 1 return button_group def add_button_box(self): """添加确认和取消按钮""" button_layout = QHBoxLayout() ok_button = QPushButton("确定") cancel_button = QPushButton("取消") ok_button.clicked.connect(self.accept) cancel_button.clicked.connect(self.reject) button_layout.addStretch() button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) self.layout.addLayout(button_layout, self.row, 0, 1, 3) self.row += 1 return ok_button, cancel_button class MainWindow(QMainWindow): """主窗口类""" def __init__(self): super().__init__() self.setWindowTitle("数字图像处理系统") self.setGeometry(100, 100, 1200, 800) # 初始化变量 self.original_image = None # 原始图像 self.processed_image = None # 处理后的图像 self.current_image = None # 当前显示的图像 self.history = [] # 操作历史 self.history_index = -1 # 当前历史位置 # 创建中心部件 self.central_widget = QWidget() self.setCentralWidget(self.central_widget) # 创建主布局 self.main_layout = QHBoxLayout(self.central_widget) # 创建左侧面板 self.left_panel = QVBoxLayout() # 创建工具栏 self.create_toolbar() # 创建图像显示区域 self.create_image_viewer() # 创建右侧面板 self.right_panel = QVBoxLayout() # 创建处理历史标签页 self.create_history_tabs() # 添加分割器 self.splitter = QSplitter(Qt.Horizontal) left_widget = QWidget() left_widget.setLayout(self.left_panel) right_widget = QWidget() right_widget.setLayout(self.right_panel) self.splitter.addWidget(left_widget) self.splitter.addWidget(right_widget) self.splitter.setSizes([800, 400]) # 初始大小 self.main_layout.addWidget(self.splitter) # 创建菜单 self.create_menu() # 状态栏 self.statusBar().showMessage("就绪") def create_menu(self): """创建菜单栏""" # 文件菜单 file_menu = self.menuBar().addMenu("文件") open_action = QAction("打开", self) open_action.setShortcut(QKeySequence.Open) open_action.triggered.connect(self.open_image) file_menu.addAction(open_action) save_action = QAction("保存", self) save_action.setShortcut(QKeySequence.Save) save_action.triggered.connect(self.save_image) file_menu.addAction(save_action) save_as_action = QAction("另存为", self) save_as_action.setShortcut(QKeySequence.SaveAs) save_as_action.setShortcut("Ctrl+Shift+S") save_as_action.triggered.connect(self.save_image_as) file_menu.addAction(save_as_action) file_menu.addSeparator() exit_action = QAction("退出", self) exit_action.setShortcut(QKeySequence.Quit) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # 编辑菜单 edit_menu = self.menuBar().addMenu("编辑") undo_action = QAction("撤销", self) undo_action.setShortcut(QKeySequence.Undo) undo_action.triggered.connect(self.undo) edit_menu.addAction(undo_action) redo_action = QAction("重做", self) redo_action.setShortcut(QKeySequence.Redo) redo_action.triggered.connect(self.redo) edit_menu.addAction(redo_action) # 处理菜单 process_menu = self.menuBar().addMenu("图像处理") # 图像转换子菜单 convert_menu = QMenu("图像转换", self) rgb_to_gray_action = QAction("RGB转灰度", self) rgb_to_gray_action.triggered.connect(self.rgb_to_gray) convert_menu.addAction(rgb_to_gray_action) resize_action = QAction("调整分辨率", self) resize_action.triggered.connect(self.resize_image) convert_menu.addAction(resize_action) process_menu.addMenu(convert_menu) # 图像增强子菜单 enhance_menu = QMenu("图像增强", self) histogram_equalization_action = QAction("直方图均衡化", self) histogram_equalization_action.triggered.connect(self.histogram_equalization) enhance_menu.addAction(histogram_equalization_action) log_transform_action = QAction("对数变换", self) log_transform_action.triggered.connect(self.log_transform) enhance_menu.addAction(log_transform_action) power_law_action = QAction("幂律变换", self) power_law_action.triggered.connect(self.power_law_transform) enhance_menu.addAction(power_law_action) blur_menu = QMenu("平滑滤波", self) mean_blur_action = QAction("均值滤波", self) mean_blur_action.triggered.connect(lambda: self.spatial_filtering("均值滤波")) blur_menu.addAction(mean_blur_action) gaussian_blur_action = QAction("高斯滤波", self) gaussian_blur_action.triggered.connect(lambda: self.spatial_filtering("高斯滤波")) blur_menu.addAction(gaussian_blur_action) median_blur_action = QAction("中值滤波", self) median_blur_action.triggered.connect(lambda: self.spatial_filtering("中值滤波")) blur_menu.addAction(median_blur_action) enhance_menu.addMenu(blur_menu) sharpen_menu = QMenu("锐化滤波", self) sobel_action = QAction("Sobel算子", self) sobel_action.triggered.connect(lambda: self.spatial_filtering("Sobel算子")) sharpen_menu.addAction(sobel_action) prewitt_action = QAction("Prewitt算子", self) prewitt_action.triggered.connect(lambda: self.spatial_filtering("Prewitt算子")) sharpen_menu.addAction(prewitt_action) laplacian_action = QAction("Laplacian算子", self) laplacian_action.triggered.connect(lambda: self.spatial_filtering("Laplacian算子")) sharpen_menu.addAction(laplacian_action) enhance_menu.addMenu(sharpen_menu) process_menu.addMenu(enhance_menu) # 图像复原子菜单 restore_menu = QMenu("图像复原", self) motion_deblur_action = QAction("运动模糊复原", self) motion_deblur_action.triggered.connect(self.motion_deblur) restore_menu.addAction(motion_deblur_action) gaussian_noise_removal_action = QAction("高斯噪声去除", self) gaussian_noise_removal_action.triggered.connect(lambda: self.noise_removal("高斯噪声")) restore_menu.addAction(gaussian_noise_removal_action) salt_pepper_noise_removal_action = QAction("椒盐噪声去除", self) salt_pepper_noise_removal_action.triggered.connect(lambda: self.noise_removal("椒盐噪声")) restore_menu.addAction(salt_pepper_noise_removal_action) process_menu.addMenu(restore_menu) # 几何变换子菜单 geometric_menu = QMenu("几何变换", self) translate_action = QAction("平移", self) translate_action.triggered.connect(self.translate_image) geometric_menu.addAction(translate_action) rotate_action = QAction("旋转", self) rotate_action.triggered.connect(self.rotate_image) geometric_menu.addAction(rotate_action) scale_action = QAction("缩放", self) scale_action.triggered.connect(self.scale_image) geometric_menu.addAction(scale_action) flip_action = QAction("镜像", self) flip_action.triggered.connect(self.flip_image) geometric_menu.addAction(flip_action) process_menu.addMenu(geometric_menu) # 形态学处理子菜单 morphology_menu = QMenu("形态学处理", self) erosion_action = QAction("腐蚀", self) erosion_action.triggered.connect(lambda: self.morphological_operation("腐蚀")) morphology_menu.addAction(erosion_action) dilation_action = QAction("膨胀", self) dilation_action.triggered.connect(lambda: self.morphological_operation("膨胀")) morphology_menu.addAction(dilation_action) opening_action = QAction("开运算", self) opening_action.triggered.connect(lambda: self.morphological_operation("开运算")) morphology_menu.addAction(opening_action) closing_action = QAction("闭运算", self) closing_action.triggered.connect(lambda: self.morphological_operation("闭运算")) morphology_menu.addAction(closing_action) edge_extraction_action = QAction("边界提取", self) edge_extraction_action.triggered.connect(self.edge_extraction) morphology_menu.addAction(edge_extraction_action) process_menu.addMenu(morphology_menu) # 图像分割子菜单 segmentation_menu = QMenu("图像分割", self) threshold_action = QAction("阈值分割", self) threshold_action.triggered.connect(self.threshold_segmentation) segmentation_menu.addAction(threshold_action) adaptive_threshold_action = QAction("自适应阈值分割", self) adaptive_threshold_action.triggered.connect(self.adaptive_threshold_segmentation) segmentation_menu.addAction(adaptive_threshold_action) watershed_action = QAction("分水岭分割", self) watershed_action.triggered.connect(self.watershed_segmentation) segmentation_menu.addAction(watershed_action) process_menu.addMenu(segmentation_menu) # 图像描述子菜单 description_menu = QMenu("图像描述", self) hu_moments_action = QAction("计算Hu不变矩", self) hu_moments_action.triggered.connect(self.calculate_hu_moments) description_menu.addAction(hu_moments_action) glcm_action = QAction("计算灰度共生矩阵", self) glcm_action.triggered.connect(self.calculate_glcm) description_menu.addAction(glcm_action) process_menu.addMenu(description_menu) # 视图菜单 view_menu = self.menuBar().addMenu("视图") zoom_in_action = QAction("放大", self) zoom_in_action.setShortcut("Ctrl++") zoom_in_action.triggered.connect(self.zoom_in) view_menu.addAction(zoom_in_action) zoom_out_action = QAction("缩小", self) zoom_out_action.setShortcut("Ctrl+-") zoom_out_action.triggered.connect(self.zoom_out) view_menu.addAction(zoom_out_action) fit_to_window_action = QAction("适应窗口", self) fit_to_window_action.setShortcut("Ctrl+F") fit_to_window_action.triggered.connect(self.fit_to_window) view_menu.addAction(fit_to_window_action) # 帮助菜单 help_menu = self.menuBar().addMenu("帮助") about_action = QAction("关于", self) about_action.triggered.connect(self.about) help_menu.addAction(about_action) help_action = QAction("帮助", self) help_action.triggered.connect(self.show_help) help_menu.addAction(help_action) def create_toolbar(self): """创建工具栏""" toolbar = QToolBar("工具栏") self.addToolBar(toolbar) # 文件操作 open_action = QAction(QIcon.fromTheme("document-open"), "打开", self) open_action.triggered.connect(self.open_image) toolbar.addAction(open_action) save_action = QAction(QIcon.fromTheme("document-save"), "保存", self) save_action.triggered.connect(self.save_image) toolbar.addAction(save_action) toolbar.addSeparator() # 编辑操作 undo_action = QAction(QIcon.fromTheme("edit-undo"), "撤销", self) undo_action.triggered.connect(self.undo) toolbar.addAction(undo_action) redo_action = QAction(QIcon.fromTheme("edit-redo"), "重做", self) redo_action.triggered.connect(self.redo) toolbar.addAction(redo_action) toolbar.addSeparator() # 视图操作 zoom_in_action = QAction(QIcon.fromTheme("zoom-in"), "放大", self) zoom_in_action.triggered.connect(self.zoom_in) toolbar.addAction(zoom_in_action) zoom_out_action = QAction(QIcon.fromTheme("zoom-out"), "缩小", self) zoom_out_action.triggered.connect(self.zoom_out) toolbar.addAction(zoom_out_action) fit_to_window_action = QAction(QIcon.fromTheme("zoom-fit-best"), "适应窗口", self) fit_to_window_action.triggered.connect(self.fit_to_window) toolbar.addAction(fit_to_window_action) toolbar.addSeparator() # 图像比较 compare_action = QAction(QIcon.fromTheme("view-compare"), "比较原图", self) compare_action.triggered.connect(self.compare_with_original) toolbar.addAction(compare_action) def create_image_viewer(self): """创建图像显示区域""" # 创建滚动区域 self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) # 创建图像查看器 self.image_viewer = ImageViewer() self.scroll_area.setWidget(self.image_viewer) # 添加到左侧面板 self.left_panel.addWidget(self.scroll_area) # 创建直方图显示区域 self.histogram_widget = HistogramWidget() self.left_panel.addWidget(self.histogram_widget) def create_history_tabs(self): """创建历史标签页""" self.history_tabs = QTabWidget() # 原始图像标签页 self.original_tab = QWidget() self.original_tab_layout = QVBoxLayout(self.original_tab) self.original_viewer = ImageViewer() self.original_tab_layout.addWidget(self.original_viewer) self.history_tabs.addTab(self.original_tab, "原始图像") # 处理后图像标签页 self.processed_tab = QWidget() self.processed_tab_layout = QVBoxLayout(self.processed_tab) self.processed_viewer = ImageViewer() self.processed_tab_layout.addWidget(self.processed_viewer) self.history_tabs.addTab(self.processed_tab, "处理后图像") # 描述信息标签页 self.info_tab = QWidget() self.info_tab_layout = QVBoxLayout(self.info_tab) self.info_text = QTextEdit() self.info_text.setReadOnly(True) self.info_tab_layout.addWidget(self.info_text) self.history_tabs.addTab(self.info_tab, "图像信息") # GLCM特征标签页 self.glcm_tab = QWidget() self.glcm_tab_layout = QVBoxLayout(self.glcm_tab) self.glcm_widget = GLCMWidget() self.glcm_tab_layout.addWidget(self.glcm_widget) self.history_tabs.addTab(self.glcm_tab, "GLCM特征") # 添加到右侧面板 self.right_panel.addWidget(self.history_tabs) def open_image(self): """打开图像文件""" file_path, _ = QFileDialog.getOpenFileName( self, "打开图像", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.gif *.tiff)" ) if file_path: self.statusBar().showMessage(f"正在加载图像: {file_path}") # 在单独的线程中加载图像,避免界面卡顿 thread = ImageProcessingThread(self._load_image, file_path) thread.finished.connect(self._on_image_loaded) thread.start() def _load_image(self, file_path): """在线程中加载图像""" image = cv2.imread(file_path) if image is None: return None, file_path return image, file_path def _on_image_loaded(self, result): """图像加载完成后的回调函数""" image, file_path = result if image is None: QMessageBox.critical(self, "错误", f"无法加载图像: {file_path}") self.statusBar().showMessage("加载图像失败") return self.original_image = image self.processed_image = image.copy() self.current_image = image.copy() # 显示图像 self.display_image(self.current_image) # 更新原始图像查看器 self.original_viewer.set_image(self.cv_to_qimage(self.original_image)) # 更新图像信息 self.update_image_info() # 清空历史 self.history = [self.original_image.copy()] self.history_index = 0 self.statusBar().showMessage(f"已加载图像: {os.path.basename(file_path)}") def save_image(self): """保存当前图像""" if self.processed_image is None: QMessageBox.warning(self, "警告", "没有可保存的图像") return if not hasattr(self, 'current_file_path'): self.save_image_as() else: try: cv2.imwrite(self.current_file_path, self.processed_image) self.statusBar().showMessage(f"已保存图像: {os.path.basename(self.current_file_path)}") except Exception as e: QMessageBox.critical(self, "错误", f"保存图像失败: {str(e)}") self.statusBar().showMessage("保存图像失败") def save_image_as(self): """另存为图像""" if self.processed_image is None: QMessageBox.warning(self, "警告", "没有可保存的图像") return file_path, _ = QFileDialog.getSaveFileName( self, "保存图像", "", "PNG (*.png);;JPEG (*.jpg);;BMP (*.bmp);;TIFF (*.tiff)" ) if file_path: try: # 确保保存的是RGB格式 if len(self.processed_image.shape) == 3: image_to_save = cv2.cvtColor(self.processed_image, cv2.COLOR_BGR2RGB) else: image_to_save = self.processed_image cv2.imwrite(file_path, image_to_save) self.current_file_path = file_path self.statusBar().showMessage(f"已保存图像: {os.path.basename(file_path)}") except Exception as e: QMessageBox.critical(self, "错误", f"保存图像失败: {str(e)}") self.statusBar().showMessage("保存图像失败") def display_image(self, image): """显示图像""" if image is None: return qimage = self.cv_to_qimage(image) self.image_viewer.set_image(qimage) # 更新直方图 self.histogram_widget.update_histogram(image) # 更新处理后图像查看器 self.processed_viewer.set_image(qimage) def cv_to_qimage(self, cv_image): """将OpenCV图像转换为Qt图像""" if len(cv_image.shape) == 3: # 彩色图像 height, width, channel = cv_image.shape bytes_per_line = 3 * width qimage = QImage(cv_image.data, width, height, bytes_per_line, QImage.Format_BGR888) else: # 灰度图像 height, width = cv_image.shape bytes_per_line = width qimage = QImage(cv_image.data, width, height, bytes_per_line, QImage.Format_Grayscale8) return qimage def update_image_info(self): """更新图像信息""" if self.original_image is None: return info = "图像信息:\n\n" info += f"尺寸: {self.original_image.shape[1]} x {self.original_image.shape[0]} 像素\n" if len(self.original_image.shape) == 3: info += f"通道数: {self.original_image.shape[2]}\n" info += "类型: 彩色图像\n" else: info += "通道数: 1\n" info += "类型: 灰度图像\n" info += f"数据类型: {self.original_image.dtype}" self.info_text.setText(info) def add_to_history(self, processed_image, operation_name): """添加操作到历史记录""" # 如果当前不在历史的末尾,删除后面的所有历史 if self.history_index < len(self.history) - 1: self.history = self.history[:self.history_index + 1] # 添加新的历史记录 self.history.append(processed_image.copy()) self.history_index += 1 # 更新处理后图像标签页 self.processed_viewer.set_image(self.cv_to_qimage(processed_image)) # 更新历史标签页标题 self.history_tabs.setTabText(1, f"处理后图像 ({operation_name})") def undo(self): """撤销操作""" if self.history_index > 0: self.history_index -= 1 self.processed_image = self.history[self.history_index].copy() self.display_image(self.processed_image) self.statusBar().showMessage("已撤销操作") def redo(self): """重做操作""" if self.history_index < len(self.history) - 1: self.history_index += 1 self.processed_image = self.history[self.history_index].copy() self.display_image(self.processed_image) self.statusBar().showMessage("已重做操作") def zoom_in(self): """放大图像""" self.image_viewer.scale_factor *= 1.1 self.image_viewer.update_pixmap() def zoom_out(self): """缩小图像""" self.image_viewer.scale_factor *= 0.9 self.image_viewer.update_pixmap() def fit_to_window(self): """适应窗口显示""" if self.current_image is None: return # 计算适应窗口的缩放因子 scroll_area_width = self.scroll_area.width() - 20 # 减去边框 scroll_area_height = self.scroll_area.height() - 20 image_width = self.current_image.shape[1] image_height = self.current_image.shape[0] scale_x = scroll_area_width / image_width scale_y = scroll_area_height / image_height self.image_viewer.scale_factor = min(scale_x, scale_y) self.image_viewer.update_pixmap() def compare_with_original(self): """比较处理后的图像与原始图像""" if self.original_image is None or self.processed_image is None: return # 创建一个新窗口进行比较 compare_window = QMainWindow() compare_window.setWindowTitle("图像比较") compare_window.resize(1000, 500) # 创建分割器 splitter = QSplitter(Qt.Horizontal) # 左侧显示原始图像 left_widget = QWidget() left_layout = QVBoxLayout(left_widget) left_label = QLabel("原始图像") left_label.setAlignment(Qt.AlignCenter) left_viewer = ImageViewer() left_viewer.set_image(self.cv_to_qimage(self.original_image)) left_layout.addWidget(left_label) left_layout.addWidget(left_viewer) # 右侧显示处理后的图像 right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_label = QLabel("处理后图像") right_label.setAlignment(Qt.AlignCenter) right_viewer = ImageViewer() right_viewer.set_image(self.cv_to_qimage(self.processed_image)) right_layout.addWidget(right_label) right_layout.addWidget(right_viewer) # 添加到分割器 splitter.addWidget(left_widget) splitter.addWidget(right_widget) splitter.setSizes([500, 500]) compare_window.setCentralWidget(splitter) compare_window.show() def about(self): """显示关于对话框""" QMessageBox.about(self, "关于数字图像处理系统", "数字图像处理系统\n\n" "基于OpenCV和PySide6开发\n" "支持图像转换、增强、复原、几何变换、形态学处理、分割和描述等功能\n\n" "版本: 1.0.0" ) def show_help(self): """显示帮助对话框""" help_text = ( "数字图像处理系统帮助文档\n\n" "1. 文件操作:\n" " - 打开: 从文件系统加载图像\n" " - 保存: 保存当前处理的图像\n" " - 另存为: 以新文件名保存图像\n\n" "2. 编辑操作:\n" " - 撤销: 撤销上一步操作\n" " - 重做: 恢复撤销的操作\n\n" "3. 图像处理:\n" " - 图像转换: 支持模式转换和分辨率调整\n" " - 图像增强: 包括直方图均衡化、滤波等\n" " - 图像复原: 处理运动模糊和噪声\n" " - 几何变换: 平移、旋转、缩放和镜像\n" " - 形态学处理: 腐蚀、膨胀、开/闭运算等\n" " - 图像分割: 阈值分割和区域分割\n" " - 图像描述: 计算不变矩和灰度共生矩阵\n\n" "4. 视图操作:\n" " - 放大/缩小: 调整图像显示大小\n" " - 适应窗口: 自动调整图像大小以适应窗口\n\n" "5. 比较功能:\n" " - 比较原图: 同时显示原始图像和处理后图像进行对比" ) QMessageBox.information(self, "帮助", help_text) # 图像处理功能实现 def rgb_to_gray(self): """RGB转灰度""" if self.processed_image is None: return if len(self.processed_image.shape) == 2: QMessageBox.warning(self, "警告", "当前图像已经是灰度图像") return thread = ImageProcessingThread(cv2.cvtColor, self.processed_image, cv2.COLOR_BGR2GRAY) thread.finished.connect(lambda result: self._process_finished(result, "RGB转灰度")) thread.start() def resize_image(self): """调整图像分辨率""" if self.processed_image is None: return dialog = ProcessingDialog("调整分辨率") # 获取当前图像尺寸 current_width = self.processed_image.shape[1] current_height = self.processed_image.shape[0] # 添加宽度和高度输入框 width_spinbox = QSpinBox() width_spinbox.setRange(1, 10000) width_spinbox.setValue(current_width) height_spinbox = QSpinBox() height_spinbox.setRange(1, 10000) height_spinbox.setValue(current_height) # 保持比例复选框 keep_ratio_checkbox = QCheckBox("保持比例") keep_ratio_checkbox.setChecked(True) # 添加到对话框 dialog.layout.addWidget(QLabel("宽度:"), 0, 0) dialog.layout.addWidget(width_spinbox, 0, 1) dialog.layout.addWidget(QLabel("像素"), 0, 2) dialog.layout.addWidget(QLabel("高度:"), 1, 0) dialog.layout.addWidget(height_spinbox, 1, 1) dialog.layout.addWidget(QLabel("像素"), 1, 2) dialog.layout.addWidget(keep_ratio_checkbox, 2, 0, 1, 3) # 添加按钮 ok_button, cancel_button = dialog.add_button_box() # 保持比例功能 ratio = current_width / current_height def update_height(): if keep_ratio_checkbox.isChecked(): height_spinbox.setValue(int(width_spinbox.value() / ratio)) def update_width(): if keep_ratio_checkbox.isChecked(): width_spinbox.setValue(int(height_spinbox.value() * ratio)) width_spinbox.valueChanged.connect(update_height) height_spinbox.valueChanged.connect(update_width) if dialog.exec_(): new_width = width_spinbox.value() new_height = height_spinbox.value() # 添加插值方法选择 interpolation_dialog = ProcessingDialog("选择插值方法") methods = ["最近邻", "双线性", "双三次", "Lanczos"] method_combo = interpolation_dialog.add_combo_box("插值方法", methods) if interpolation_dialog.exec_(): method_index = method_combo.currentIndex() interpolation_methods = [ cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_LANCZOS4 ] interpolation = interpolation_methods[method_index] thread = ImageProcessingThread(cv2.resize, self.processed_image, (new_width, new_height), interpolation=interpolation) thread.finished.connect(lambda result: self._process_finished(result, "调整分辨率")) thread.start() def histogram_equalization(self): """直方图均衡化""" if self.processed_image is None: return if len(self.processed_image.shape) == 3: # 彩色图像需要先转换到YUV空间 def equalize_color(image): yuv = cv2.cvtColor(image, cv2.COLOR_BGR2YUV) yuv[:, :, 0] = cv2.equalizeHist(yuv[:, :, 0]) return cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR) thread = ImageProcessingThread(equalize_color, self.processed_image) else: # 灰度图像直接均衡化 thread = ImageProcessingThread(cv2.equalizeHist, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "直方图均衡化")) thread.start() def log_transform(self): """对数变换""" if self.processed_image is None: return dialog = ProcessingDialog("对数变换") c_slider, c_spinbox = dialog.add_slider("常数C", 1, 100, 25) if dialog.exec_(): c = c_spinbox.value() def log_transform(image): # 确保图像是浮点类型 image_float = image.astype(np.float32) / 255.0 # 应用对数变换 result = c * np.log(1 + image_float) # 归一化到[0, 1] result = cv2.normalize(result, None, 0, 1, cv2.NORM_MINMAX) # 转回uint8 return (result * 255).astype(np.uint8) thread = ImageProcessingThread(log_transform, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "对数变换")) thread.start() def power_law_transform(self): """幂律变换""" if self.processed_image is None: return dialog = ProcessingDialog("幂律变换") gamma_slider, gamma_spinbox = dialog.add_double_slider("伽马值", 0.1, 5.0, 1.0, 0.1, 1) if dialog.exec_(): gamma = gamma_spinbox.value() def power_law_transform(image): # 确保图像是浮点类型 image_float = image.astype(np.float32) / 255.0 # 应用幂律变换 result = np.power(image_float, gamma) # 归一化到[0, 1] result = cv2.normalize(result, None, 0, 1, cv2.NORM_MINMAX) # 转回uint8 return (result * 255).astype(np.uint8) thread = ImageProcessingThread(power_law_transform, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"幂律变换 (γ={gamma})")) thread.start() def spatial_filtering(self, filter_type): """空域滤波""" if self.processed_image is None: return dialog = ProcessingDialog(f"{filter_type}参数设置") if filter_type in ["均值滤波", "高斯滤波"]: kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3, 2) # 奇数 elif filter_type == "中值滤波": kernel_size, _ = dialog.add_slider("孔径大小", 1, 21, 3, 2) # 奇数 elif filter_type in ["Sobel算子", "Prewitt算子"]: direction_combo = dialog.add_combo_box("方向", ["水平", "垂直", "两者"]) elif filter_type == "Laplacian算子": ksize_combo = dialog.add_combo_box("核大小", ["1", "3", "5", "7"], 1) if dialog.exec_(): if filter_type == "均值滤波": ksize = kernel_size.value() def mean_filter(image): return cv2.blur(image, (ksize, ksize)) thread = ImageProcessingThread(mean_filter, self.processed_image) elif filter_type == "高斯滤波": ksize = kernel_size.value() def gaussian_filter(image): return cv2.GaussianBlur(image, (ksize, ksize), 0) thread = ImageProcessingThread(gaussian_filter, self.processed_image) elif filter_type == "中值滤波": ksize = kernel_size.value() def median_filter(image): return cv2.medianBlur(image, ksize) thread = ImageProcessingThread(median_filter, self.processed_image) elif filter_type == "Sobel算子": direction = direction_combo.currentIndex() def sobel_filter(image): if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if direction == 0: # 水平 sobelx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3) return cv2.convertScaleAbs(sobelx) elif direction == 1: # 垂直 sobely = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3) return cv2.convertScaleAbs(sobely) else: # 两者 sobelx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3) sobely = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3) return cv2.addWeighted(cv2.convertScaleAbs(sobelx), 0.5, cv2.convertScaleAbs(sobely), 0.5, 0) thread = ImageProcessingThread(sobel_filter, self.processed_image) elif filter_type == "Prewitt算子": direction = direction_combo.currentIndex() def prewitt_filter(image): if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) kernelx = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=np.float32) kernely = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]], dtype=np.float32) if direction == 0: # 水平 prewittx = cv2.filter2D(image, -1, kernelx) return prewittx elif direction == 1: # 垂直 prewitty = cv2.filter2D(image, -1, kernely) return prewitty else: # 两者 prewittx = cv2.filter2D(image, -1, kernelx) prewitty = cv2.filter2D(image, -1, kernely) return cv2.addWeighted(prewittx, 0.5, prewitty, 0.5, 0) thread = ImageProcessingThread(prewitt_filter, self.processed_image) elif filter_type == "Laplacian算子": ksize = [1, 3, 5, 7][ksize_combo.currentIndex()] def laplacian_filter(image): if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) return cv2.Laplacian(image, cv2.CV_64F, ksize=ksize) thread = ImageProcessingThread(laplacian_filter, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, filter_type)) thread.start() def motion_deblur(self): """运动模糊复原""" if self.processed_image is None: return dialog = ProcessingDialog("运动模糊复原参数设置") length_slider, length_spinbox = dialog.add_slider("运动长度", 1, 100, 15) angle_slider, angle_spinbox = dialog.add_slider("运动角度", 0, 360, 0) gamma_slider, gamma_spinbox = dialog.add_double_slider("噪声功率谱比", 0.01, 10.0, 0.1, 0.01, 2) if dialog.exec_(): length = length_spinbox.value() angle = angle_spinbox.value() gamma = gamma_spinbox.value() def deblur(image): # 确保图像是灰度图 if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 创建运动模糊核 kernel = np.zeros((length, length), dtype=np.float32) kernel[int((length - 1) / 2), :] = np.ones(length, dtype=np.float32) # 旋转核 M = cv2.getRotationMatrix2D((length / 2, length / 2), angle, 1.0) kernel = cv2.warpAffine(kernel, M, (length, length)) # 归一化 kernel /= length # 傅里叶变换 fft = np.fft.fft2(gray) fft_kernel = np.fft.fft2(kernel, s=gray.shape) # 维纳滤波 H_conj = np.conj(fft_kernel) H_squared = np.abs(fft_kernel) ** 2 G = (H_conj / (H_squared + gamma)) * fft # 逆傅里叶变换 deblurred = np.fft.ifft2(G) deblurred = np.abs(deblurred) # 归一化到0-255 deblurred = cv2.normalize(deblurred, None, 0, 255, cv2.NORM_MINMAX) deblurred = deblurred.ast(np.uint8) return deblurred thread = ImageProcessingThread(deblur, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "运动模糊复原")) thread.start() def noise_removal(self, noise_type): """噪声去除""" if self.processed_image is None: return dialog = ProcessingDialog(f"{noise_type}去除") if noise_type == "高斯噪声": kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3, 2) # 奇数 elif noise_type == "椒盐噪声": kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3, 2) # 奇数 if dialog.exec_(): ksize = kernel_size.value() if noise_type == "高斯噪声": def remove_gaussian_noise(image): return cv2.GaussianBlur(image, (ksize, ksize), 0) thread = ImageProcessingThread(remove_gaussian_noise, self.processed_image) elif noise_type == "椒盐噪声": def remove_salt_pepper_noise(image): return cv2.medianBlur(image, ksize) thread = ImageProcessingThread(remove_salt_pepper_noise, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"{noise_type}去除")) thread.start() def translate_image(self): """平移图像""" if self.processed_image is None: return dialog = ProcessingDialog("平移图像") tx_slider, tx_spinbox = dialog.add_slider("水平偏移", -500, 500, 0) ty_slider, ty_spinbox = dialog.add_slider("垂直偏移", -500, 500, 0) if dialog.exec_(): tx = tx_spinbox.value() ty = ty_spinbox.value() def translate(image): M = np.float32([[1, 0, tx], [0, 1, ty]]) return cv2.warpAffine(image, M, (image.shape[1], image.shape[0])) thread = ImageProcessingThread(translate, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"平移 (tx={tx}, ty={ty})")) thread.start() def rotate_image(self): """旋转图像""" if self.processed_image is None: return dialog = ProcessingDialog("旋转图像") angle_slider, angle_spinbox = dialog.add_slider("旋转角度", -180, 180, 0) scale_slider, scale_spinbox = dialog.add_double_slider("缩放比例", 0.1, 5.0, 1.0, 0.1, 1) if dialog.exec_(): angle = angle_spinbox.value() scale = scale_spinbox.value() def rotate(image): center = (image.shape[1] // 2, image.shape[0] // 2) M = cv2.getRotationMatrix2D(center, angle, scale) return cv2.warpAffine(image, M, (image.shape[1], image.shape[0])) thread = ImageProcessingThread(rotate, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"旋转 ({angle}°, 缩放{scale}x)")) thread.start() def scale_image(self): """缩放图像""" if self.processed_image is None: return dialog = ProcessingDialog("缩放图像") scale_slider, scale_spinbox = dialog.add_double_slider("缩放比例", 0.1, 5.0, 1.0, 0.1, 1) if dialog.exec_(): scale = scale_spinbox.value() def scale_image(image): new_width = int(image.shape[1] * scale) new_height = int(image.shape[0] * scale) return cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_LINEAR) thread = ImageProcessingThread(scale_image, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"缩放 ({scale}x)")) thread.start() def flip_image(self): """镜像图像""" if self.processed_image is None: return dialog = ProcessingDialog("镜像图像") flip_type = dialog.add_combo_box("镜像类型", ["水平", "垂直", "水平和垂直"]) if dialog.exec_(): flip_code = flip_type.currentIndex() def flip(image): return cv2.flip(image, flip_code) flip_types = ["水平", "垂直", "水平和垂直"] thread = ImageProcessingThread(flip, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"镜像 ({flip_types[flip_code]})")) thread.start() def morphological_operation(self, operation_type): """形态学操作""" if self.processed_image is None: return dialog = ProcessingDialog(f"{operation_type}参数设置") kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3) kernel_shape = dialog.add_combo_box("核形状", ["矩形", "椭圆", "十字形"]) if dialog.exec_(): ksize = kernel_size.value() shape_index = kernel_shape.currentIndex() kernel_shapes = [ cv2.MORPH_RECT, cv2.MORPH_ELLIPSE, cv2.MORPH_CROSS ] kernel = cv2.getStructuringElement(kernel_shapes[shape_index], (ksize, ksize)) if operation_type == "腐蚀": def erode(image): return cv2.erode(image, kernel) thread = ImageProcessingThread(erode, self.processed_image) elif operation_type == "膨胀": def dilate(image): return cv2.dilate(image, kernel) thread = ImageProcessingThread(dilate, self.processed_image) elif operation_type == "开运算": def opening(image): return cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel) thread = ImageProcessingThread(opening, self.processed_image) elif operation_type == "闭运算": def closing(image): return cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel) thread = ImageProcessingThread(closing, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, operation_type)) thread.start() def edge_extraction(self): """边界提取""" if self.processed_image is None: return dialog = ProcessingDialog("边界提取参数设置") kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3) kernel_shape = dialog.add_combo_box("核形状", ["矩形", "椭圆", "十字形"]) if dialog.exec_(): ksize = kernel_size.value() shape_index = kernel_shape.currentIndex() kernel_shapes = [ cv2.MORPH_RECT, cv2.MORPH_ELLIPSE, cv2.MORPH_CROSS ] kernel = cv2.getStructuringElement(kernel_shapes[shape_index], (ksize, ksize)) def extract_edge(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 膨胀 dilated = cv2.dilate(gray, kernel) # 边界提取 return dilated - gray thread = ImageProcessingThread(extract_edge, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "边界提取")) thread.start() def threshold_segmentation(self): """阈值分割""" if self.processed_image is None: return dialog = ProcessingDialog("阈值分割参数设置") threshold_slider, threshold_spinbox = dialog.add_slider("阈值", 0, 255, 127) max_value_slider, max_value_spinbox = dialog.add_slider("最大值", 0, 255, 255) threshold_type = dialog.add_combo_box("阈值类型", [ "二进制阈值", "反二进制阈值", "截断阈值", "零阈值", "反零阈值", "Otsu算法" ]) if dialog.exec_(): threshold = threshold_spinbox.value() max_value = max_value_spinbox.value() type_index = threshold_type.currentIndex() threshold_types = [ cv2.THRESH_BINARY, cv2.THRESH_BINARY_INV, cv2.THRESH_TRUNC, cv2.THRESH_TOZERO, cv2.THRESH_TOZERO_INV, cv2.THRESH_BINARY + cv2.THRESH_OTSU ] def threshold_segment(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() if type_index == 5: # Otsu算法,忽略手动设置的阈值 _, thresh = cv2.threshold(gray, 0, max_value, threshold_types[type_index]) else: _, thresh = cv2.threshold(gray, threshold, max_value, threshold_types[type_index]) return thresh type_names = ["二进制阈值", "反二进制阈值", "截断阈值", "零阈值", "反零阈值", "Otsu算法"] thread = ImageProcessingThread(threshold_segment, self.processed_image) thread.finished.connect( lambda result: self._process_finished(result, f"阈值分割 ({type_names[type_index]})")) thread.start() def adaptive_threshold_segmentation(self): """自适应阈值分割""" if self.processed_image is None: return dialog = ProcessingDialog("自适应阈值分割参数设置") max_value_slider, max_value_spinbox = dialog.add_slider("最大值", 0, 255, 255) method_combo = dialog.add_combo_box("自适应方法", ["均值", "高斯"]) type_combo = dialog.add_combo_box("阈值类型", ["二进制阈值", "反二进制阈值"]) block_size_slider, block_size_spinbox = dialog.add_slider("块大小", 3, 101, 11, 2) # 奇数 c_slider, c_spinbox = dialog.add_double_slider("常数C", -10, 10, 2, 0.1, 1) if dialog.exec_(): max_value = max_value_spinbox.value() method_index = method_combo.currentIndex() type_index = type_combo.currentIndex() block_size = block_size_spinbox.value() c = c_spinbox.value() adaptive_methods = [ cv2.ADAPTIVE_THRESH_MEAN_C, cv2.ADAPTIVE_THRESH_GAUSSIAN_C ] threshold_types = [ cv2.THRESH_BINARY, cv2.THRESH_BINARY_INV ] def adaptive_threshold(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() return cv2.adaptiveThreshold( gray, max_value, adaptive_methods[method_index], threshold_types[type_index], block_size, c ) method_names = ["均值", "高斯"] type_names = ["二进制阈值", "反二进制阈值"] thread = ImageProcessingThread(adaptive_threshold, self.processed_image) thread.finished.connect(lambda result: self._process_finished( result, f"自适应阈值分割 ({method_names[method_index]}, {type_names[type_index]})" )) thread.start() def watershed_segmentation(self): """分水岭分割""" if self.processed_image is None: return if len(self.processed_image.shape) != 3: QMessageBox.warning(self, "警告", "分水岭分割需要彩色图像") return dialog = ProcessingDialog("分水岭分割参数设置") threshold_slider, threshold_spinbox = dialog.add_slider("阈值", 0, 255, 100) morph_size_slider, morph_size_spinbox = dialog.add_slider("形态学操作核大小", 1, 21, 3) if dialog.exec_(): threshold = threshold_spinbox.value() morph_size = morph_size_spinbox.value() def watershed(image): # 转换为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 阈值处理 ret, thresh = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 噪声去除 kernel = np.ones((morph_size, morph_size), np.uint8) opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2) # 确定背景区域 sure_bg = cv2.dilate(opening, kernel, iterations=3) # 确定前景区域 dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5) ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # 找到未知区域 sure_fg = np.uint8(sure_fg) unknown = cv2.subtract(sure_bg, sure_fg) # 标记标签 ret, markers = cv2.connectedComponents(sure_fg) # 为所有标签加1,确保背景不是0而是1 markers = markers + 1 # 将未知区域标记为0 markers[unknown == 255] = 0 # 应用分水岭算法 markers = cv2.watershed(image, markers) image[markers == -1] = [0, 0, 255] # 标记边界为红色 return image thread = ImageProcessingThread(watershed, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "分水岭分割")) thread.start() def calculate_hu_moments(self): """计算Hu不变矩""" if self.processed_image is None: return def calculate_moments(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 计算矩 moments = cv2.moments(gray) # 计算Hu不变矩 hu_moments = cv2.HuMoments(moments) # 对数变换,方便显示 for i in range(7): hu_moments[i] = -1 * np.copysign(1.0, hu_moments[i]) * np.log10(np.abs(hu_moments[i])) return hu_moments thread = ImageProcessingThread(calculate_moments, self.processed_image) thread.finished.connect(self._on_hu_moments_calculated) thread.start() def _on_hu_moments_calculated(self, hu_moments): """Hu不变矩计算完成后的回调""" result = "Hu不变矩:\n\n" for i, moment in enumerate(hu_moments): result += f"H{i + 1}: {moment[0]:.8f}\n" self.info_text.setText(result) self.history_tabs.setCurrentWidget(self.info_tab) self.statusBar().showMessage("Hu不变矩计算完成") def calculate_glcm(self): """计算灰度共生矩阵""" if self.processed_image is None: return thread = ImageProcessingThread(self._calculate_glcm_thread, self.processed_image) thread.finished.connect(self._on_glcm_calculated) thread.start() def _calculate_glcm_thread(self, image): """在线程中计算GLCM""" if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 量化到16级灰度以减少计算量 gray_quantized = (gray // 16).astype(np.uint8) # 计算GLCM矩阵 (距离=1, 角度=0, 45, 90, 135度) glcm = graycomatrix(gray_quantized, distances=[1], angles=[0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], levels=16, symmetric=True, normed=True) return glcm def _on_glcm_calculated(self, glcm): """GLCM计算完成后的回调""" self.glcm_widget.update_glcm(self.processed_image) self.history_tabs.setCurrentWidget(self.glcm_tab) self.statusBar().showMessage("灰度共生矩阵计算完成") def _process_finished(self, result, operation_name): """图像处理完成后的回调""" self.processed_image = result self.display_image(result) self.add_to_history(result, operation_name) self.statusBar().showMessage(f"{operation_name}完成") if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())为什么运行后打开图片D:\python\python.exe D:\PythonProject\.venv\2.py 插入图片后闪退后显示 进程已结束,退出代码为 -1073740791 (0xC0000409)
06-09
package main import ( "fmt" "image" "image/color" "log" "net/http" "os" "path/filepath" "runtime" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "main.go/dataModel/CookieModel" "main.go/dataModel/ShopModel" "main.go/dataModel/SkuModel" "main.go/dataModel/UserModel" "main.go/res" "main.go/tuuz/database" ) // 全局状态 type AppState struct { Window fyne.Window CurrentUser UserModel.UserInfo Shops []ShopModel.Account ProductTabs *container.AppTabs StatusBar *widget.Label ShopListBinding binding.UntypedList LoginForm *widget.Form LeftPanel *fyne.Container // 改为存储整个左侧面板容器 FilterFilePath string FilterKeywords []string ShopListPanel *fyne.Container // 新增:存储店铺列表面板 FilterPanel *fyne.Container // 存储过滤面板引用 KeywordCount *widget.Label // 存储关键字计数标签 TabShopMap map[string]ShopModel.Account // 新增:存储标签页与店铺的映射 SplitContainer *container.Split // 新增:保存分割布局引用 TopPanel *fyne.Container // 新增:保存顶部面板引用 ContentPanel *fyne.Container // 新增:保存内容面板引用 NeedsRefresh bool // 新增:状态变更标志 LastRefreshTime time.Time // 新增:最后刷新时间 } // 添加状态检查快捷键 func addStateDebugShortcut(window fyne.Window, appState *AppState) { window.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) { if ev.Name == fyne.KeyF5 { // 手动刷新UI refreshLeftPanel(appState) appState.StatusBar.SetText("手动刷新UI") } else if ev.Name == fyne.KeyS { // 打印状态快照 fmt.Println("===== 应用状态快照 =====") fmt.Printf("当前用户: %s\n", appState.CurrentUser.LoginName) fmt.Printf("店铺数量: %d\n", len(appState.Shops)) fmt.Printf("最后刷新时间: %s\n", appState.LastRefreshTime.Format("15:04:05.000")) fmt.Printf("左侧面板: %T\n", appState.LeftPanel) fmt.Printf("分割布局: %T\n", appState.SplitContainer) fmt.Println("=======================") } }) } func main() { os.Setenv("PLAYWRIGHT_BROWSERS_PATH", "./browsers") database.Init() UserModel.UserInit() ShopModel.ShopInit() CookieModel.CreateCookieInfoTable() SkuModel.ProductInit() myApp := app.New() myWindow := myApp.NewWindow("店铺管理工具") myWindow.Resize(fyne.NewSize(1200, 800)) // 初始化应用状态 appState := &AppState{ FilterFilePath: getDefaultFilterPath(), TabShopMap: make(map[string]ShopModel.Account), // 初始化映射 LastRefreshTime: time.Now(), } // 注册调试快捷键 addStateDebugShortcut(myWindow, appState) // 启动状态监听器 startStateListener(appState) // 尝试加载默认过滤文件 go loadFilterFile(appState) // 创建状态栏 appState.StatusBar = widget.NewLabel("就绪") statusBar := container.NewHBox(layout.NewSpacer(), appState.StatusBar) // 创建主布局 mainContent := createMainUI(myWindow, appState) // 设置整体布局 content := container.NewBorder( nil, // 顶部 statusBar, // 底部 nil, // 左侧 nil, // 右侧 mainContent, ) myWindow.SetContent(content) // 启动时尝试自动登录 go tryAutoLogin(appState) myWindow.ShowAndRun() } // 新增状态监听器 - 定期检查状态变化 func startStateListener(appState *AppState) { go func() { for { time.Sleep(100 * time.Millisecond) // 每100ms检查一次 if appState.NeedsRefresh { fyne.DoAndWait(func() { refreshLeftPanel(appState) appState.NeedsRefresh = false }) } } }() } // 获取默认过滤文件路径 func getDefaultFilterPath() string { if runtime.GOOS == "windows" { return filepath.Join(os.Getenv("USERPROFILE"), "Documents", "filter.txt") } return filepath.Join(os.Getenv("HOME"), "filter.txt") } // 修改 refreshAllProductTabs 函数 func refreshAllProductTabs(appState *AppState) { if appState.ProductTabs == nil || len(appState.ProductTabs.Items) == 0 { return } // 遍历所有标签页并刷新 for _, tab := range appState.ProductTabs.Items { // 通过标签页标题获取店铺 shop, exists := appState.TabShopMap[tab.Text] if !exists { continue } // 重新加载商品 go func(shop ShopModel.Account) { products, err := loadProductsForShop(shop, appState) if err != nil { fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("刷新 %s 商品失败: %s", shop.AccountName, err.Error())) }) return } fyne.DoAndWait(func() { // 更新标签页内容 tab.Content = container.NewMax(createProductList(products)) appState.ProductTabs.Refresh() appState.StatusBar.SetText(fmt.Sprintf("已刷新 %s 的商品", shop.AccountName)) }) }(shop) } } // 加载过滤文件 func loadFilterFile(appState *AppState) { if appState.FilterFilePath == "" { return } if _, err := os.Stat(appState.FilterFilePath); os.IsNotExist(err) { err := os.WriteFile(appState.FilterFilePath, []byte{}, 0644) if err != nil { log.Printf("创建过滤文件失败: %v", err) } return } content, err := os.ReadFile(appState.FilterFilePath) if err != nil { log.Printf("读取过滤文件失败: %v", err) return } lines := strings.Split(string(content), "\n") appState.FilterKeywords = []string{} for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { appState.FilterKeywords = append(appState.FilterKeywords, trimmed) } } fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个过滤关键字", len(appState.FilterKeywords))) // 更新关键字数量标签 if appState.KeywordCount != nil { // 修正为 KeywordCount appState.KeywordCount.SetText(fmt.Sprintf("关键字数量: %d", len(appState.FilterKeywords))) } // 刷新所有已打开的商品标签页 refreshAllProductTabs(appState) }) } // 修改 createMainUI 函数 - 保存分割布局引用 func createMainUI(window fyne.Window, appState *AppState) fyne.CanvasObject { appState.Window = window // 创建整个左侧面板 leftPanel := createLeftPanel(window, appState) appState.LeftPanel = leftPanel // 右侧面板 appState.ProductTabs = container.NewAppTabs() appState.ProductTabs.SetTabLocation(container.TabLocationTop) rightPanel := container.NewBorder( widget.NewLabelWithStyle("商品信息", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), nil, nil, nil, container.NewMax(appState.ProductTabs), ) // 使用HSplit布局 - 保存引用 split := container.NewHSplit(leftPanel, rightPanel) split.SetOffset(0.25) appState.SplitContainer = split // 保存分割布局引用 return split } // 修改createFilterPanel函数 - 返回容器并保存引用 func createFilterPanel(appState *AppState) *fyne.Container { // 创建文件路径标签 pathLabel := widget.NewLabel("过滤文件: " + appState.FilterFilePath) pathLabel.Wrapping = fyne.TextWrapWord // 创建选择文件按钮 selectButton := widget.NewButton("选择过滤文件", func() { dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { if err != nil { dialog.ShowError(err, appState.Window) return } if reader == nil { return // 用户取消 } // 更新文件路径 appState.FilterFilePath = reader.URI().Path() pathLabel.SetText("过滤文件: " + appState.FilterFilePath) // 加载过滤文件 go func() { loadFilterFile(appState) // 刷新所有已打开的商品标签页 refreshAllProductTabs(appState) }() }, appState.Window) }) // 创建刷新按钮 refreshButton := widget.NewButton("刷新过滤", func() { if appState.FilterFilePath != "" { appState.StatusBar.SetText("刷新过滤关键字...") go func() { loadFilterFile(appState) // 刷新所有已打开的商品标签页 refreshAllProductTabs(appState) }() } else { appState.StatusBar.SetText("请先选择过滤文件") } }) // 创建按钮容器 buttonContainer := container.NewHBox( selectButton, refreshButton, ) // 创建关键字计数标签 - 保存引用 keywordCount := widget.NewLabel(fmt.Sprintf("关键字数量: %d", len(appState.FilterKeywords))) keywordCount.TextStyle = fyne.TextStyle{Bold: true} appState.KeywordCount = keywordCount // 创建面板容器 panel := container.NewVBox( widget.NewSeparator(), widget.NewLabelWithStyle("商品过滤", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), pathLabel, keywordCount, buttonContainer, ) return panel } // 修改 createLoggedInPanel 函数 - 确保注销时直接刷新 func createLoggedInPanel(appState *AppState) fyne.CanvasObject { return container.NewVBox( widget.NewLabelWithStyle("登录状态", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), widget.NewSeparator(), container.NewHBox( widget.NewLabel("用户:"), widget.NewLabel(appState.CurrentUser.LoginName), ), container.NewHBox( widget.NewLabel("店铺数量:"), widget.NewLabel(fmt.Sprintf("%d", len(appState.Shops))), ), widget.NewSeparator(), container.NewCenter( widget.NewButton("注销", func() { // 重置状态 appState.CurrentUser = UserModel.UserInfo{} appState.Shops = nil appState.ProductTabs.Items = nil appState.ProductTabs.Refresh() appState.TabShopMap = make(map[string]ShopModel.Account) // 直接调用刷新函数 refreshLeftPanel(appState) appState.StatusBar.SetText("已注销") }), ), ) } // 重构创建顶部面板函数 - 确保状态正确反映 func createTopPanel(appState *AppState) *fyne.Container { // 添加调试日志 fmt.Printf("创建顶部面板: 登录状态=%t, 用户名=%s\n", appState.CurrentUser.LoginName != "", appState.CurrentUser.LoginName) var content fyne.CanvasObject if appState.CurrentUser.LoginName != "" { content = createLoggedInPanel(appState) } else { content = createLoginForm(appState) } return container.NewMax(content) } // 重构 createContentPanel 函数 - 添加详细日志 func createContentPanel(appState *AppState) *fyne.Container { // 添加详细调试日志 fmt.Printf("创建内容面板: 登录状态=%t, 用户名=%s, 店铺数量=%d\n", appState.CurrentUser.LoginName != "", appState.CurrentUser.LoginName, len(appState.Shops)) if appState.CurrentUser.LoginName != "" { if len(appState.Shops) > 0 { return createShopListPanel(appState) } return container.NewCenter( widget.NewLabel("没有可用的店铺"), ) } return container.NewCenter( widget.NewLabel("请先登录查看店铺列表"), ) } // 重构刷新函数 - 确保完全重建UI func refreshLeftPanel(appState *AppState) { if appState.SplitContainer == nil { return } // 添加详细调试信息 fmt.Printf("刷新左侧面板 - 时间: %s, 用户: %s, 店铺数量: %d\n", time.Now().Format("15:04:05.000"), appState.CurrentUser.LoginName, len(appState.Shops)) // 创建新的左侧面板 newLeftPanel := createLeftPanel(appState.Window, appState) // 添加调试背景色(登录状态不同颜色不同) var debugColor color.Color if appState.CurrentUser.LoginName != "" { debugColor = color.NRGBA{R: 0, G: 100, B: 0, A: 30} // 登录状态绿色半透明 } else { debugColor = color.NRGBA{R: 100, G: 0, B: 0, A: 30} // 未登录状态红色半透明 } debugPanel := container.NewMax( canvas.NewRectangle(debugColor), newLeftPanel, ) // 替换分割布局中的左侧面板 appState.SplitContainer.Leading = debugPanel appState.LeftPanel = debugPanel // 刷新分割布局 appState.SplitContainer.Refresh() // 强制重绘整个窗口 appState.Window.Content().Refresh() appState.LastRefreshTime = time.Now() } // 重构 createLeftPanel 函数 - 确保使用正确的状态 func createLeftPanel(window fyne.Window, appState *AppState) *fyne.Container { // 创建顶部面板(用户状态/登录表单) topPanel := createTopPanel(appState) // 创建内容面板(店铺列表或提示) contentPanel := createContentPanel(appState) // 创建过滤面板 filterPanel := createFilterPanel(appState) // 使用Border布局 return container.NewBorder( topPanel, // 顶部 filterPanel, // 底部 nil, nil, // 左右 contentPanel, // 中间内容 ) } // 修改登录按钮回调 - 确保状态正确更新 func createLoginForm(appState *AppState) fyne.CanvasObject { usernameEntry := widget.NewEntry() passwordEntry := widget.NewPasswordEntry() usernameEntry.PlaceHolder = "输入邮箱地址" passwordEntry.PlaceHolder = "输入密码" // 登录按钮回调 loginButton := widget.NewButton("登录", func() { appState.StatusBar.SetText("登录中...") go func() { // 模拟网络延迟 time.Sleep(500 * time.Millisecond) // 获取店铺信息 shops := ShopModel.Api_select_struct(nil) fyne.DoAndWait(func() { if len(shops) == 0 { appState.StatusBar.SetText("获取店铺信息为空") return } // 更新应用状态 appState.Shops = shops appState.CurrentUser, _ = UserModel.Api_find_by_username(usernameEntry.Text) // 添加状态更新日志 fmt.Printf("登录成功 - 用户: %s, 店铺数量: %d\n", appState.CurrentUser.LoginName, len(appState.Shops)) if appState.CurrentUser.LoginName == "" { appState.CurrentUser.LoginName = "1" } appState.StatusBar.SetText(fmt.Sprintf("登录成功! 共 %d 个店铺", len(shops))) // 直接刷新UI refreshLeftPanel(appState) }) }() }) form := widget.NewForm( widget.NewFormItem("邮箱:", usernameEntry), widget.NewFormItem("密码:", passwordEntry), ) appState.LoginForm = form return container.NewVBox( widget.NewLabelWithStyle("登录面板", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), form, container.NewCenter(loginButton), ) } // 修改自动登录函数 - 添加详细日志 func tryAutoLogin(appState *AppState) { // 获取所有用户 users := UserModel.Api_select_struct(nil) if len(users) == 0 { fyne.DoAndWait(func() { appState.StatusBar.SetText("获取已经存在的账号为空") }) return } // 尝试使用第一个用户自动登录 user := users[0] fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("尝试自动登录: %s...", user.LoginName)) }) // 获取用户名输入框 if appState.LoginForm == nil || len(appState.LoginForm.Items) < 2 { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 登录表单尚未初始化") }) return } usernameItem := appState.LoginForm.Items[0] usernameEntry, ok := usernameItem.Widget.(*widget.Entry) if !ok { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 用户名控件类型错误") }) return } passwordItem := appState.LoginForm.Items[1] passwordEntry, ok := passwordItem.Widget.(*widget.Entry) if !ok { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 密码控件类型错误") }) return } // 触发登录 fyne.DoAndWait(func() { usernameEntry.SetText(user.LoginName) passwordEntry.SetText(user.LoginPass) appState.StatusBar.SetText("正在自动登录...") // 更新应用状态 appState.CurrentUser = user appState.Shops = ShopModel.Api_select_struct(nil) // 添加自动登录日志 fmt.Printf("自动登录成功 - 用户: %s, 店铺数量: %d\n", appState.CurrentUser.LoginName, len(appState.Shops)) // 直接刷新UI refreshLeftPanel(appState) }) } // 修改后的异步加载店铺头像函数 func loadShopAvatar(img *canvas.Image, url string) { if url == "" { // 使用默认头像 fyne.DoAndWait(func() { img.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "account") img.Refresh() }) return } // 创建HTTP客户端(可设置超时) client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { log.Printf("加载头像失败: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("头像请求失败: %s", resp.Status) return } // 解码图片 imgData, _, err := image.Decode(resp.Body) if err != nil { log.Printf("解码头像失败: %v", err) return } // 在主线程更新UI fyne.DoAndWait(func() { img.Image = imgData img.Refresh() }) } // 修改后的 createShopListPanel 函数 func createShopListPanel(appState *AppState) *fyne.Container { // 创建绑定数据 if appState.ShopListBinding == nil { appState.ShopListBinding = binding.NewUntypedList() } else { // 确保绑定数据是最新的 updateShopListBinding(appState) } // 创建列表控件 list := widget.NewListWithData( appState.ShopListBinding, func() fyne.CanvasObject { avatar := canvas.NewImageFromResource(nil) avatar.SetMinSize(fyne.NewSize(40, 40)) avatar.FillMode = canvas.ImageFillContain nameLabel := widget.NewLabel("") statusIcon := widget.NewIcon(nil) return container.NewHBox( avatar, container.NewVBox(nameLabel), layout.NewSpacer(), statusIcon, ) }, func(item binding.DataItem, obj fyne.CanvasObject) { hbox, ok := obj.(*fyne.Container) if !ok || len(hbox.Objects) < 4 { return } avatar, _ := hbox.Objects[0].(*canvas.Image) nameContainer, _ := hbox.Objects[1].(*fyne.Container) nameLabel, _ := nameContainer.Objects[0].(*widget.Label) statusIcon, _ := hbox.Objects[3].(*widget.Icon) val, err := item.(binding.Untyped).Get() if err != nil { return } shop, ok := val.(ShopModel.Account) if !ok { return } nameLabel.SetText(shop.AccountName) if shop.CanLogin { statusIcon.SetResource(res.ResShuffleSvg) } else { statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error")) } go loadShopAvatar(avatar, shop.AccountAvatar) }, ) list.OnSelected = func(id widget.ListItemID) { if id < 0 || id >= len(appState.Shops) { return } shop := appState.Shops[id] appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName)) go func() { products, err := loadProductsForShop(shop, appState) if err != nil { fyne.DoAndWait(func() { appState.StatusBar.SetText("加载商品失败: " + err.Error()) }) return } fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products))) addOrUpdateProductTab(appState, shop, products) }) }() } // 创建滚动容器 - 设置最小高度确保可滚动 scrollContainer := container.NewScroll(list) scrollContainer.SetMinSize(fyne.NewSize(280, 200)) // 最小高度200确保可滚动 // 使用Max容器确保填充空间 return container.NewMax( container.NewBorder( widget.NewLabel("店铺列表"), nil, nil, nil, scrollContainer, ), ) } // 更新店铺列表绑定数据 func updateShopListBinding(appState *AppState) { if appState.ShopListBinding == nil { appState.ShopListBinding = binding.NewUntypedList() } values := make([]interface{}, len(appState.Shops)) for i, shop := range appState.Shops { values[i] = shop } appState.ShopListBinding.Set(values) } // 应用商品过滤 func applyProductFilter(products []SkuModel.DataItem, keywords []string) []SkuModel.DataItem { if len(keywords) == 0 { return products // 没有关键字,返回所有商品 } filtered := []SkuModel.DataItem{} for _, product := range products { exclude := false for _, keyword := range keywords { if strings.Contains(strings.ToLower(product.Name), strings.ToLower(keyword)) { exclude = true break } } if !exclude { filtered = append(filtered, product) } } return filtered } // 为店铺加载商品数据 func loadProductsForShop(shop ShopModel.Account, appState *AppState) ([]SkuModel.DataItem, error) { // 模拟API调用获取商品数据 time.Sleep(500 * time.Millisecond) // 模拟网络延迟 // 模拟返回数据 products := []SkuModel.DataItem{ {ProductID: "1001", Name: "高端智能手机", MarketPrice: 99900, DiscountPrice: 100}, {ProductID: "1002", Name: "无线蓝牙耳机", MarketPrice: 199900, DiscountPrice: 50}, {ProductID: "1003", Name: "智能手表", MarketPrice: 299900, DiscountPrice: 30}, {ProductID: "1004", Name: "平板电脑", MarketPrice: 399900, DiscountPrice: 20}, {ProductID: "1005", Name: "笔记本电脑", MarketPrice: 499900, DiscountPrice: 10}, } // 应用过滤 filteredProducts := applyProductFilter(products, appState.FilterKeywords) return filteredProducts, nil } // 修改 addOrUpdateProductTab 函数 - 确保商品列表填充标签页空间 func addOrUpdateProductTab(appState *AppState, shop ShopModel.Account, products []SkuModel.DataItem) { tabTitle := shop.AccountName // 检查是否已存在该TAB for _, tab := range appState.ProductTabs.Items { if tab.Text == tabTitle { // 更新现有TAB tab.Content = container.NewMax(createProductList(products)) // 更新映射 appState.TabShopMap[tabTitle] = shop appState.ProductTabs.Refresh() return } } // 创建新TAB newTab := container.NewTabItem( tabTitle, container.NewMax(createProductList(products)), ) // 添加到映射 appState.TabShopMap[tabTitle] = shop appState.ProductTabs.Append(newTab) appState.ProductTabs.Select(newTab) } // 创建商品列表 - 修复表格填充问题 func createProductList(products []SkuModel.DataItem) fyne.CanvasObject { // 创建表格 table := widget.NewTable( func() (int, int) { return len(products) + 1, 4 // 行数=商品数+表头,列数=4 }, func() fyne.CanvasObject { return widget.NewLabel("模板文本") }, func(id widget.TableCellID, cell fyne.CanvasObject) { label := cell.(*widget.Label) if id.Row == 0 { // 表头 switch id.Col { case 0: label.SetText("商品ID") case 1: label.SetText("商品名称") case 2: label.SetText("价格") case 3: label.SetText("库存") } label.TextStyle.Bold = true return } // 数据行 product := products[id.Row-1] switch id.Col { case 0: label.SetText(product.ProductID) case 1: label.SetText(product.Name) case 2: label.SetText(fmt.Sprintf("¥%.2f", float64(product.MarketPrice)/100)) case 3: label.SetText(fmt.Sprintf("%d", product.DiscountPrice)) } }, ) // 设置列宽 table.SetColumnWidth(0, 100) table.SetColumnWidth(1, 300) table.SetColumnWidth(2, 100) table.SetColumnWidth(3, 100) // 创建滚动容器 scrollContainer := container.NewScroll(table) scrollContainer.SetMinSize(fyne.NewSize(600, 400)) // 返回可滚动的表格容器 return scrollContainer } 这是我修改后已经解决了之前问题的代码,接下来,我需要修改商品列表的UI,需要在商品列表的下面添加分页组件和页面跳转按钮,同时需要模拟更多的SkuModel.DataItem来测试分页效果。该怎么修改代码?
最新发布
07-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值