pyqt股票行情软件性能优化 差点又让python背了锅

因为对c++一直处于差不多能看的懂代码,但写的话一头包,所以毅然采用pyqt编写一个股票行情软件。部分窗体截取如下:

pyqt股票行情软件性能优化 差点又让python背了锅_第1张图片

等大体上快完工了,跑着跑着突然发现,界面卡顿的一笔。一看cpu,飚到了十几。瞅瞅人家的行情软件,那cpu使用都是稳定的在2以下。

行情一频繁就尿裤,难道是py太拉胯了?

于是立马使用cProfile:

python -m cProfile -s cumulative main.py

 

1.一号锅:拉胯的setStyleSheet

首先发现setStyleSheet这个函数耗时太久,单发调用居然要5ms,而股票嘛,有红有绿,这调用又非常频繁,故而这肯定是一个瓶颈。 网上搜索了一下,setStyleSheet会触发上级组件的重绘,所以性能上无比拉胯。

不用setStyleSheet修改文字颜色,那就用别的方案。有用palette的,我试了下,毫无效果,并且又注意到官方说不保证palette在所有平台上都一致,所以palette作废。

另外,还可以给QLabel设置html代替纯文本,于是简单撸了以下工具函数

def set_label_text_with_style(label, text, style):
    label.setText(f"
{text}
")

对代码进行全局替换。

再一跑,似乎cpu调用下降了,但还是8左右,也不能100%确定这到底有没有起到性能上的优化作用。

于是继续查看下cProfile的输出结果,终于发现了罪魁祸首:QListWidget

2.二号锅:无比拉胯的QListWidget

QListWidget这兄弟,如果数据变化不频繁的话,那还是很能罩的住场子的,譬如什么好友列表,音乐播放列表。但是碰到高速行情,就不行了。大量的ListItem的添加,删除,QListWidget的性能就开始跟不上了。

于是继续网上搜索,又说用QListView的,但是那文档实在是稀烂,而且有各种各样的api。我不就是显示个行情,几个数字而已,何必搞那么复杂。还是自己撸个类似的ListView。

3.自己撸个ListView

现在,我需要个显示效果和QListWidget一致,但是在高频行情下,能够光速刷新,毫不尿裤的listview。它对外暴露一个数据model,用户只需要往里面增删数据,即可同步刷新到界面ui。另外,还需要暴露一个delegate,列表项绘制的时候,就调用用户提供的绘图代码,对列表项进行绘制。

class FFList(QScrollArea):

    def __init__(self, model, parent=None):
        super().__init__(parent)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.content_view = ContentView(model, self)
        self.setWidget(self.content_view)
        self.content_view.setFixedHeight(1)
        self.verticalScrollBar().setStyleSheet('''
            QScrollBar:vertical {
                width: 15px;
            }
        ''')

    def resizeEvent(self, e):
        self.content_view.setFixedWidth(self.width())

    def scrollToBottom(self):
        scroll_bar = self.verticalScrollBar()
        scroll_bar.setValue(scroll_bar.maximum())

这便是自己撸的listview了,取名为FFList。它继承自QScrollArea, 这样滚动条的相关逻辑还是交由qt进行处理。另外,FFList只支持垂直滚动,水平滚动也是同理,不赘述。

class FFListModel:

    def __init__(self, delegate, max_count):
        self.items = []
        self.delegate = delegate
        self.max_count = max_count
        self.__content_view = None

    def attach_content_view(self, content_view):
        self.__content_view = content_view

    def __notify_update(self):
        if self.__content_view is not None:
            self.__content_view.refresh_ui()

    def clear(self):
        self.items = []
        self.__notify_update()

    def add(self, *items):
        self.items.extend(items)
        if len(self.items) > self.max_count:
            self.items = self.items[len(self.items) - self.max_count:]
        self.__notify_update()

    def pop(self, count):
        self.items = self.items[count:]
        self.__notify_update()

这便是数据类型model了,当数据发生变化的时候,调用__notify_update对上层ui进行告知,进而更新ui显示。

class FFItemDelegate:

    def item_height(self):
        raise NotImplemented()

    def paint(self, qp, item, x, y, w, h):
        raise NotImplemented()

这是列表项绘制的代理类,需要用户实例化

class ContentView(QWidget):

    def __init__(self, model, ff_list):
        super().__init__(ff_list)
        self.ff_list = ff_list
        self.model = model
        self.model.attach_content_view(self)

    def refresh_ui(self):
        item_height = self.model.delegate.item_height()
        self.setFixedHeight(len(self.model.items) * item_height)
        # print(len(self.model.items) * item_height)
        self.update()

    def paintEvent(self, e):
        item_height = self.model.delegate.item_height()
        item_width = self.width()
        v_y0 = self.ff_list.verticalScrollBar().value()
        v_y1 = v_y0 + self.ff_list.viewport().height()
        i0 = v_y0 // item_height
        i1 = v_y1 // item_height
        if i1 >= len(self.model.items):
            i1 = len(self.model.items) - 1
        qp = QPainter()
        qp.begin(self)
        for i in range(i0, i1 + 1):
            self.model.delegate.paint(qp, self.model.items[i], 0, i * item_height, item_width, item_height)
        qp.end()

ContentView是最重要的一个类,他是FFList,即QScrollArea的centralWidget。每当数据变动时,都会调用refresh_ui, 重新设置高度(这一步会改动滚动条)

每当窗口需要重绘时,qt会调用paintEvent,这个函数的实现是自制的FFList性能上拉不拉胯的关键。

首先,计算v_y0,这就是当前滚动条滚到哪的这个位置。v_y1是v_y0加上视口高度,所以v_y0到v_y1之间的内容,就是当前滚动区域可视的高度区间(因为这只是垂直滚动,故而水平的不用管)。

接下来对可视区间内的列表项进行绘制,它会直接调用用户提供的绘制代码。

如果我们不计算v_y0, v_y1,而是对所有列表项都进行绘制,那么对不可见的列表项进行绘制,就是性能上的极大浪费了。

4.FFList的使用

使用上就比较简单了。列表组件由原先继承QListWidget改为FFList,并初始化model:

class OrderInternalView(FFList):

    def __init__(self, parent=None):
        self.model = FFListModel(OrderItemDelegate(), 200)
        super().__init__(self.model, parent)

当有新的行情的时候,将其转化为数据类,然后将其添加到model里:

self.model.add(*items)

另外,需要实现绘制接口,大致如下(根据业务需求自绘):

class OrderItemDelegate(FFItemDelegate):

    def item_height(self):
        return 20

    def paint(self, qp, item, x, y, w, h):
        if item.seq == 0:
            qp.setPen(lightGrayPen)
            time_text = to_time_label_str(item.timestamp)
        else:
            qp.setPen(goldPen)
            time_text = str(item.seq + 1)
        qp.drawText(x, y, 0.26 * w, h, Qt.AlignRight, time_text)
        x += 0.26 * w

        qp.setPen(QPen(QColor(get_color(item.price, item.prev_close))))
        qp.drawText(x, y, 0.28 * w, h, Qt.AlignRight, "%.2f" % item.price)
        x += 0.28 * w

        vol_pen = QPen(QColor(get_color(item.type, item.side)))
        if item.vol > 500:
            qp.setPen(purplePen)
        else:
            qp.setPen(vol_pen)
        qp.drawText(x, y, 0.2 * w, h, Qt.AlignRight, "%.0f" % item.vol)
        x += 0.2 * w

        qp.setPen(vol_pen)
        qp.drawText(x, y, 0.14 * w, h, Qt.AlignRight, to_symbol(item.type, item.side))
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

5.优化结果

使用自定义的FFList后,cpu使用稳定在2%以下,虽然说仍然略为拉胯,但已经算是可用了。其他的优化便是细枝末节的了,在不影响使用的前提下就不进行进一步的优化了。

 

 

你可能感兴趣的:(qt)