rxts 发表于 4 天前

pyside6子类无法接收父类发射的正确值

以下是父类EXPANDABLE_MENU.py的代码:
from CHROMELESS_WINDOW import *

# 伸缩菜单 父类
class ExpandableMenu(ChromelessWindow):
    list_index = Signal(dict)   # 发射 鼠标移动时的位置   {序号:[矩形起始y,矩形结束y,文字起始y,文字结束y]}
    scrollbar_start_y = Signal(object)# 发射滚动条的起始y
    text_gps_dict = Signal(dict)      # 发射文本的gps
    press_left = Signal(int)   # 发射 按下左键时的文本序号
    not_text_area = Signal(str)   # # 发射 '非文本区域按下左键'
    text_order = Signal(QPointF, object)    # 调整文本顺序时发射 参数 鼠标轨迹,文本索引
    def __init__(self, parent=None):
      super().__init__(parent)
      self.is_left_pressed = False      # 左键按住标志
      self.__left = None                  # 记录按下左键的位置
      self.press_left_y = None            # 按住左键时 滚动条的起始y
      self.__pos = None    # 设置 是否按住左键调整文本顺序 同时也时鼠标的移动轨迹
      self.ctrl_left_name = None            # 标记 调整文本顺序时的 名字

      # 调用父类属性
      self.min_button_tf, self.max_button_tf, self.button_close_tf = False, False, False   # 设置三大组件失灵
      self.drive_window = False# 为真时窗口可拖动

      self.__list_index = None      # 鼠标所在的位置的文本名字
      self.parameters_settings()# 初始化各项参数

    def parameters_settings(self, min_view: bool = True,                # 设置 本窗口在任务栏不可见
                            adj_up: bool = False,
                            adj_down: bool = False,
                            adj_left: bool = False,                     # 左 为假时不可调整大小
                            adj_right: bool = True,                     # 右
                            window_x: int = 100,
                            window_y: int = 50,
                            window_width: int = 200,
                            text_number: int = 20,                      # 要显示文本数量
                            text_sum: int = 100,                        # 文本的总长度
                            text_size: int | float = 10,                # 字体大小
                            text_lenght: int | float = 2,               # 字体做坐标间距
                            text_window_start_y: int | float = 100,      # 用于显示文本区域的起始x位置 非绘制文字的空白区域总高度
                            header_color: str = '#C4A737',             # 标头颜色'#C4A737'
                            move_create_color: str = '#CEA5DA',      # 跟随鼠标矩形的颜色
                            # column_width: dict = None,    # 每一列的宽度{类序号:宽度}
                            scrollbar_color: str = '#444343',         # 滚动条颜色
                            scrollbar_area_color: str = '#FAF8F8',      # 滚动条区域的颜色
                            backcolor: str = None,                      # 背景颜色
                            ):
      if min_view:
            self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool)# 设置窗口在任务栏中不可见
      if backcolor:
            self.setStyleSheet(f"background-color: {backcolor};")   # 设置背景颜色

      self.adj_up = adj_up
      self.adj_down = adj_down
      self.adj_left = adj_left
      self.adj_right = adj_right

      self.__window_x = window_x
      self.__window_y = window_y
      self.__window_width = window_width
      self.__text_number = text_number                  # 要显示文本数量
      self.__text_sum = text_sum                        # 文本的总长度
      self.__text_size = text_size                        # 字体大小
      self.__text_size_ratio = text_lenght                # 字体间距比例
      self.__text_lenght = text_lenght * self.__text_size    # 字体纵坐标间距
      # self.__column_width = {0: 50} if column_width is None else column_width# 每一列的宽度{类序号:宽度}
      self.__scrollbar_color = scrollbar_color            # 滚动条颜色
      self.scrollbar_area_color = scrollbar_area_color    # 滚动条区域颜色

      screen = QApplication.primaryScreen().geometry()
      self.__screenWidth, self.__screenHeight = screen.width(), screen.height()
      self.__scrollbar_width = self.__screenWidth * 0.01                  # 滚动条宽度
      self.__scrollbar_ratio = self.__text_number / self.__text_sum       # 滚动条占比
      self.__text_window_start_y = text_window_start_y                  # 用于显示文本区域的起始x位置
      self.__header_color = header_color                                  # 标头颜色
      self.move_create_color = move_create_color                        # 跟随鼠标矩形的颜色
      self.text_start_index = 0                                           # 文本的开始索引
      self.text_end_index = self.__text_number                            # 文本的结束索引

      # 初始化 窗口
      a = self.__text_lenght if header_color else 0
      self.setGeometry(self.__window_x,
                         self.__window_y,
                         self.__window_width,
                         a + self.__text_number * self.__text_lenght + self.__text_window_start_y)
      # 文本结束y和跟随鼠标的矩形y的距离
      self.text_entity_lenght = self.__text_size * ((self.__text_size_ratio - 1) / 2)
      if self.__text_window_start_y:# 空白区域
            if header_color:
                # 显示文本内容的区域 的起始y
                self.__text_start_y = self.__text_size * self.__text_size_ratio + self.__text_window_start_y

                # 滚动条起始y
                self.__scrollbar_start_y = self.__text_start_y
                self.__scrollbar_start_x = self.width() - self.__scrollbar_width# 滚动条起始x
                # 滚动条长度
                self.__scrollbar_height = self.__scrollbar_ratio * (self.height() - self.__scrollbar_start_y)
                self.__scrollbar_end_y = self.__text_start_y + self.__scrollbar_height# 滚动条结束y
            else:
                self.__text_start_y = self.__text_window_start_y      # # 显示文本内容的区域 的起始y

                self.__scrollbar_start_y = self.__text_window_start_y   # 滚动条起始y
                self.__scrollbar_start_x = self.width() - self.__scrollbar_width# 滚动条起始x
                # 滚动条长度
                self.__scrollbar_height = self.__scrollbar_ratio * (self.height() - self.__text_window_start_y)
                self.__scrollbar_end_y = self.__scrollbar_start_y + self.__scrollbar_height# 滚动条结束y
      else:
            if header_color:
                self.__text_start_y = self.__text_size * self.__text_size_ratio# 显示文本内容的区域 的起始y

                self.__scrollbar_start_y = self.__text_start_y                      # 滚动条起始y
                self.__scrollbar_start_x = self.width() - self.__scrollbar_width    # 滚动条起始x
                # 滚动条长度
                self.__scrollbar_height = self.__scrollbar_ratio * (self.height() - self.__scrollbar_start_y)
                self.__scrollbar_end_y = self.__scrollbar_start_y + self.__scrollbar_height# 滚动条结束y
            else:
                self.__text_start_y = 0         # 显示文本内容的区域 的起始y
                self.expandable_scrollbar_gps()# 初始化滚动条gps
      self.scrollbar_max_start_y = self.height() - self.__scrollbar_height# 限制滚动论最大的起始y

      self.__text_list =     # 生成和文本列表同等长度的列表
      self.expandable_text_gps_dict(start_index=0, end_index=self.__text_number)# 初始化跟随鼠标的矩形gps

    def expandable_text_gps_dict(self, start_index, end_index):
      self.mapping_text_gps_dict = {}   # {序号:[矩形起始y,矩形结束y,文字起始y,文字结束y]}
      for index, s in enumerate(self.__text_list):
            end_y = self.__text_start_y + (index + 1) * self.__text_lenght    # 矩形结束y
            self.mapping_text_gps_dict = [self.__text_start_y + index * self.__text_lenght,
                                             end_y,
                                             end_y - self.text_entity_lenght - self.__text_size,
                                             end_y - self.text_entity_lenght]
      self.text_gps_dict.emit(self.mapping_text_gps_dict)
      print(self.mapping_text_gps_dict)
    def mouseMoveEvent(self, event):# 鼠标移动
      super().mouseMoveEvent(event)
      pos = event.position()
      if self.is_left_pressed is False and pos.x() < self.__scrollbar_start_x:# 跟随鼠标的矩形
            if pos.y() > self.__text_start_y:
                for key_number, val_gps in self.mapping_text_gps_dict.items():
                  if val_gps < pos.y() < val_gps:
                        self.__list_index = key_number
                        # 发射 鼠标移动时的位置   {序号:[矩形起始y,矩形结束y,文字起始y,文字结束y]}
                        self.list_index.emit({self.__list_index: val_gps})
                        self.update()
                        break
                  else:
                        self.__list_index = None
      elif self.is_left_pressed and pos.x() < self.__scrollbar_start_x:# 调整文本显示的位置
            self.__pos = pos    # 设置 是否按住左键调整文本顺序 同时也时鼠标的移动轨迹
            self.text_order.emit(self.__pos, self.__text_list)    # 参数 鼠标轨迹,文本索引
            self.update()

      elif self.is_left_pressed and pos.y() > self.__text_start_y and pos.x() > self.__scrollbar_start_x:
            # 更新滚动条
            new_y = pos.y() - self.__left   # 计算鼠标移动和按下左键时的距离
            new_y = self.press_left_y + new_y   # 按住左键时滚动条的位置 + 鼠标移动的距离
            if self.__text_start_y <= new_y <= self.scrollbar_max_start_y:
                self.__scrollbar_start_y = new_y
                text_start_y_ratio = (new_y - self.__text_start_y) / (self.height() - self.__text_start_y - self.text_entity_lenght)
                self.text_start_index = int(text_start_y_ratio * self.__text_sum)
                self.text_end_index = self.text_start_index + self.__text_number
                self.expandable_text_gps_dict(self.text_start_index,
                                              self.text_end_index)# {序号:[矩形起始y,矩形结束y,文字起始y,文字结束y]}
                self.scrollbar_start_y.emit(new_y)# 发射滚动条的起始y
                self.update()

    def mousePressEvent(self, event: QMouseEvent):# 鼠标案件
      super().mousePressEvent(event)
      pos = event.position()

      if event.button() == Qt.LeftButton:# 左键
            self.ctrl_left_name = self.__list_index   # 标记 调整文本顺序时的 名字
            self.press_left_y = self.__scrollbar_start_y# 按住左键时 滚动条的 起始y位置
            self.__left = pos.y()   # 记录按下左键的位置
            self.is_left_pressed = True# 设置按下标志
            if pos.x() < self.__scrollbar_start_x and pos.y() > self.__text_start_y:
                if self.__list_index:
                  self.press_left.emit(self.__list_index)   # 发射 按下左键时的文本序号
            elif pos.y() < self.__text_start_y:
                self.not_text_area.emit('非文本区域按下左键')    # 发射 '非文本区域'

      elif event.button() == Qt.RightButton:# 右键
            pass

    def mouseReleaseEvent(self, event):
      super().mouseReleaseEvent(event)
      pos = event.position()
      if event.button() == Qt.LeftButton:
            self.is_left_pressed = False# 清除按下标志
            if self.__pos:
                for key_number, val_gps in self.mapping_text_gps_dict.items():
                  if val_gps < pos.y() < val_gps:
                        self.__list_index = self.ctrl_left_name
                        # 发射 鼠标移动时的位置   {序号:[矩形起始y,矩形结束y,文字起始y,文字结束y]}
                        self.list_index.emit({self.__list_index: val_gps})
                        self.update()
                        break
                  else:
                        self.__list_index = None
                self.__pos = None# 设置 是否按住左键调整文本顺序
                pass



    def wheelEvent(self, event):# 鼠标滚轮
      super().wheelEvent(event)
      angle_delta = event.angleDelta().y()# 垂直滚动的量
      len_text = len(self.__text_list)
      self.text_start_index, self.text_end_index = wheel_update_index(text_len=len_text,
                                                                        max_number=self.__text_number,
                                                                        start_index=self.text_start_index,
                                                                        end_index=self.text_end_index,
                                                                        angle_delta=angle_delta)
      a = self.text_start_index / len_text    # 开始索引占中长度的比例
      b = self.height() - self.__text_start_y   # 显示区域的长度
      self.__scrollbar_start_y = a * b + self.__text_start_y         # 更新滚动条起始y
      self.expandable_text_gps_dict(start_index=self.text_start_index, end_index=self.text_end_index)
      self.update()

    def expandable_scrollbar_gps(self):
      self.__scrollbar_start_y = 0                                                    # 滚动条起始y
      self.__scrollbar_start_x = self.width() - self.__scrollbar_width                # 滚动条起始x
      self.__scrollbar_height = self.__scrollbar_ratio * self.height()                # 滚动条长度
      self.__scrollbar_end_y = self.__scrollbar_start_y + self.__scrollbar_height   # 滚动条结束y

    def paintEvent(self, event):
      qp = QPainter(self)
      # 开启抗锯齿
      qp.setRenderHint(QPainter.Antialiasing)# PyQt5/PySide6 通用写法
      # 可选:同时开启其他渲染优化
      qp.setRenderHint(QPainter.TextAntialiasing)# 文字抗锯齿
      # qp.setRenderHint(QPainter.SmoothPixmapTransform)# 图片平滑缩放

      # 绘制滚动条区域
      entity_painter(qp, self.scrollbar_area_color, 0).drawRect(self.__scrollbar_start_x,
                                                                  self.__text_start_y,
                                                                  self.__scrollbar_width,
                                                                  self.height() - self.__text_start_y)
      qp.restore()

      # 绘制滚动条 移动的轨迹
      entity_painter(qp, self.__scrollbar_color, 0).drawRect(self.__scrollbar_start_x,
                                                               self.__scrollbar_start_y
                                                               , self.__scrollbar_width
                                                               , self.__scrollbar_height)
      qp.restore()

      # 绘制跟随鼠标的矩形
      if self.__list_index is not None:
            try:
                entity_painter(qp, self.move_create_color, 0).drawRect(0,
                                                      self.mapping_text_gps_dict,
                                                      self.width() - self.__scrollbar_width,
                                                      self.__text_lenght)
                qp.restore()
            except KeyError:
                pass

      if self.__pos:   # 绘制 调整文本顺序
            # 绘制调整文本顺序时 矩形的移动轨迹
            entity_painter(qp, self.move_create_color, 0).drawRect(0,
                                                self.__pos.y() - self.__text_lenght / 2,
                                                self.width() - self.__scrollbar_width,
                                                self.__text_lenght)
            qp.restore()
以下是子类a.py的代码:
from EXPANDABLE_MENU import *
import sys
from data_json import *
from MAPPING import *

# 我的自选
class MyMatchList(ExpandableMenu):
    def __init__(self, parent=None):
      super().__init__(parent)

      screen = QApplication.primaryScreen().geometry()
      self.match_width, self.match_height = screen.width(), screen.height()
      self.match_font_size = self.match_height * 0.015    # 字体大小
      self.text_x_lenght = self.match_font_size * 3       # 文本横向坐标的距离
      self.match_text_start_y = self.match_height * 0.19# 显示文本区域的起始y,空白区域的结束y
      self.match_window_width = int(self.match_width * 0.3)   # 初始化 窗口宽度
      self.match_font_type = "SimSun"                         # 字体类型
      self.text_y_lenght = 2                                  # 文本纵向坐标的间距

      # 创建与父类的链接
      self.text_gps_dict.connect(self.js_text_gps)# 接收文本的纵向坐标gps



      # 标头母版
      self.header_mother_set = ['代码/名字', '价格', '涨幅', '大宗交易', '成交额', '流通市值']
      # 数据母版
      self.data_mother_set = ['SH600000_野马电池', '00.00', '++00.00', '20250909', '0.00亿', '000.00亿']
      self.match_data_list = []   # 我的自选数据列表

      self.match_window(window_x=0, window_y=0)   # 初始化窗口 以及各项参数

    def js_text_gps(self, gps: dict):
      '''接收文本的纵向坐标gps'''
      self.match_text_y_gps = gps
      print(gps)

    def match_window(self, window_x: int = 0, window_y: int = 0):
      '''初始化窗口'''
      match_json = Data_Http.mymatch_set_data()
      # 文本列表
      self.match_text_list = match_json['match'] if 'match' in match_json else None
      # 窗口尺寸
      self.match_window_width = int(match_json['window_size']) if 'window_size' in match_json else int(self.match_width * 0.3)

      if self.match_text_list is None:
            self.match_text_list = self.data_mother_set
      # 窗口中可以显示的数据列数
      self.display_column_number = mother_set_calculate(
                                                            data=self.match_text_list,            # 数据
                                                            start_x=self.match_font_size * 2,       # 预留 距离窗口左边的距离
                                                            window_width=self.match_window_width,   # 窗口宽度
                                                            font_type=self.match_font_type,         # 字体类型
                                                            font_size=self.match_font_size,         # 字体大小
                                                            x_lenght=self.text_x_lenght             # 文本横向距离
                                                            )
      # 文本的横向gps          标头的横向gps
      self.match_text_x_gps, self.match_header_gps = mother_set_mapping_gps(
                           header=self.header_mother_set,         # 标头
                           data=self.match_text_list,               # 数据
                           start_x=self.match_font_size * 2,      # 预留 距离窗口左边的距离
                           font_type=self.match_font_type,          # 字体类型
                           font_size=self.match_font_size,          # 字体大小
                           x_lenght=self.text_x_lenght            # 文本横向距离
                           )
      # 调用父类
      self.parameters_settings(window_x=window_x,
                            window_y=window_y,
                            window_width=self.match_window_width,
                            text_number=20,                              # 要显示文本数量
                            text_sum=len(self.match_text_list),            # 文本的总长度
                            text_size=self.match_font_size,                # 字体大小
                            text_lenght=self.text_y_lenght,                # 字体y坐标间距
                            text_window_start_y=self.match_text_start_y      # 用于显示文本区域的起始x位置
                            )


    def resizeEvent(self, event):   # 监控窗口大小
      super().resizeEvent(event)# 确保父类逻辑执行
      # old_size = event.oldSize()# 旧尺寸 (QSize)
      new_size = event.size()   # 新尺寸 (QSize)

      if self.match_data_list:
            pass

    # 接收实时数据的接口
    def mymatchlist_data_API(self, data: list = None) -> None:
      self.match_data_list = data   # 我的自选数据列表
      len_data = len(data)
      self.parameters_settings(window_x=100,
                            window_y=50,
                            window_width=200,
                            text_number=20,                      # 要显示文本数量
                            text_sum=len_data,                        # 文本的总长度
                            text_size=self.match_font_size,                # 字体大小
                            text_lenght=2,               # 字体做坐标间距
                            text_window_start_y=self.match_text_start_y      # 用于显示文本区域的起始x位置
                            )
      self.update()
    def paintEvent(self, event):
      super().paintEvent(event)
      qp = QPainter(self)
      #

      # 绘制标头
      for index, i in enumerate(self.header_mother_set):
            text_begin(self, qp, '#BC29E5', "SimSun", self.match_font_size,
                     self.match_header_gps,
                     self.match_text_start_y - self.match_font_size * ((self.text_y_lenght - 1) / 2),
                     f'{i}')
            qp.restore()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.processEvents()# 处理未完成的事件
    window = MyMatchList()
    window.show()
    sys.exit(app.exec())
print(gps)语句的打印值为{},但是我通过在父类的发射信号处self.text_gps_dict.emit(self.mapping_text_gps_dict)添加
打印语句print(self.mapping_text_gps_dict),发现父类可以打印处正确的值,当父类的值改变时print(gps)会跟随打印出
{}而父类的打印语句正确无误,这说明父类发射了正确的值,子类MyMatchList也接收到了信号只不过收到的是空字典,这是怎么回事

FishC 发表于 4 天前

你的问题在于子类 `MyMatchList` 在接收父类 `ExpandableMenu` 的 `text_gps_dict` 信号时收到了空字典 `{}`。核心原因是 **信号连接时机太晚**,父类在初始化过程中已经发射了信号,但此时子类尚未连接该信号。以下是详细分析和解决方案:

### 问题分析
1. **父类初始化流程**:
   class ExpandableMenu(ChromelessWindow):
       def __init__(self, parent=None):
         super().__init__(parent)
         self.parameters_settings()# 这里会调用 expandable_text_gps_dict() 并发射信号
   - 父类在 `__init__` 中直接调用 `parameters_settings()`
   - `parameters_settings()` 内部会调用 `expandable_text_gps_dict()` 发射 `text_gps_dict` 信号

2. **子类初始化流程**:
   class MyMatchList(ExpandableMenu):
       def __init__(self, parent=None):
         super().__init__(parent)# 父类构造器先执行(此时已发射信号)
         self.text_gps_dict.connect(self.get_gps)# 子类在此后才连接信号
   - 当 `super().__init__` 执行时,父类已经发射了信号
   - 子类的信号连接 (`connect`) 发生在父类初始化**之后**,因此错过了初始信号

---

### 解决方案
#### 方法1:延迟父类的信号发射(推荐)
修改父类,在 `parameters_settings()` 中**移除初始化发射**,改为单独调用:
# 修改父类 EXPANDABLE_MENU.py
class ExpandableMenu(ChromelessWindow):
    def parameters_settings(self, ...):
      # ... 其他代码保持不变 ...
      # 移除下面这行:
      # self.expandable_text_gps_dict(start_index=0, end_index=self.__text_number)
   
    # 新增方法供子类调用
    def init_text_gps(self):
      self.expandable_text_gps_dict(start_index=0, end_index=self.__text_number)

在子类中显式初始化位置字典:
class MyMatchList(ExpandableMenu):
    def __init__(self, parent=None):
      super().__init__(parent)
      self.text_gps_dict.connect(self.get_gps)# 先连接信号
      
      # 手动初始化文本位置字典
      self.init_text_gps()# 调用父类新增的方法
      
    def set_data(self, data: list = None):
      # ... 原有代码 ...
      self.init_text_gps()# 数据更新后重新初始化位置

#### 方法2:在子类中主动触发更新
如果不想修改父类,可在子类连接信号后主动请求数据:
class MyMatchList(ExpandableMenu):
    def __init__(self, parent=None):
      super().__init__(parent)
      self.text_gps_dict.connect(self.get_gps)
      
      # 主动触发父类更新
      self.expandable_text_gps_dict(0, self._ExpandableMenu__text_number)

---

### 关键点说明
1. **执行顺序问题**:
   - Python 构造器执行顺序:父类 `__init__` → 子类 `__init__`
   - 父类在构造时发射的信号,子类尚未连接,因此丢失

2. **代码修复原则**:
   - 父类构造时自动发射信号(子类错过)
   + 子类准备好后手动触发信号发射(确保连接有效)

3. **访问父类私有变量**:
   若需访问父类私有变量如 `__text_number`,使用:
   value = self._ExpandableMenu__text_number# 双下划线变量名重整规则

---

### 验证步骤
1. 在子类 `get_gps` 槽函数添加调试打印:
   def get_gps(self, gps):
       print("Received GPS:", gps)# 此时应收到非空字典
2. 确保每次数据更新后调用 `init_text_gps()`

通过调整信号发射时机,即可解决子类接收空字典的问题。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
页: [1]
查看完整版本: pyside6子类无法接收父类发射的正确值