因爲對c++一直處於差不多能看的懂代碼,但寫的話一頭包,所以毅然採用pyqt編寫一個股票行情軟件。部分窗體截取如下:
等大體上快完工了,跑着跑着突然發現,界面卡頓的一筆。一看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"<div style='{style}'>{text}</div>")
對代碼進行全局替換。
再一跑,似乎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))
5.優化結果
使用自定義的FFList後,cpu使用穩定在2%以下,雖然說仍然略爲拉胯,但已經算是可用了。其他的優化便是細枝末節的了,在不影響使用的前提下就不進行進一步的優化了。