因为对c++一直处于差不多能看的懂代码,但写的话一头包,所以毅然采用pyqt编写一个股票行情软件。部分窗体截取如下:
等大体上快完工了,跑着跑着突然发现,界面卡顿的一笔。一看cpu,飚到了十几。瞅瞅人家的行情软件,那cpu使用都是稳定的在2以下。
行情一频繁就尿裤,难道是py太拉胯了?
于是立马使用cProfile:
python -m cProfile -s cumulative main.py
首先发现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
QListWidget这兄弟,如果数据变化不频繁的话,那还是很能罩的住场子的,譬如什么好友列表,音乐播放列表。但是碰到高速行情,就不行了。大量的ListItem的添加,删除,QListWidget的性能就开始跟不上了。
于是继续网上搜索,又说用QListView的,但是那文档实在是稀烂,而且有各种各样的api。我不就是显示个行情,几个数字而已,何必搞那么复杂。还是自己撸个类似的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,而是对所有列表项都进行绘制,那么对不可见的列表项进行绘制,就是性能上的极大浪费了。
使用上就比较简单了。列表组件由原先继承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))
使用自定义的FFList后,cpu使用稳定在2%以下,虽然说仍然略为拉胯,但已经算是可用了。其他的优化便是细枝末节的了,在不影响使用的前提下就不进行进一步的优化了。