pyqt股票行情軟件性能優化 差點又讓python背了鍋

因爲對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))
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

5.優化結果

使用自定義的FFList後,cpu使用穩定在2%以下,雖然說仍然略爲拉胯,但已經算是可用了。其他的優化便是細枝末節的了,在不影響使用的前提下就不進行進一步的優化了。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章