Repository: vnpy/vnpy

Issue:
使用 QTextEdit 显示日志
## 环境

* 操作系统: windows 10
* Python版本: python 3.7.9
* vn.py版本: v2.1.9.1

## Issue类型
三选一：Enhancement

## 预期程序行为
UI 日志显示单条内容 wrap

## 实际程序行为
UI 日志显示有时会横向滚动

## 重现步骤
CTA策略 UI 中日志消息内容单条稍长时, 横向滚动 scroll bar 行为异常




Discussion:

Resolving patch:
diff --git a/vnpy/app/cta_strategy/ui/widget.py b/vnpy/app/cta_strategy/ui/widget.py
--- a/vnpy/app/cta_strategy/ui/widget.py
+++ b/vnpy/app/cta_strategy/ui/widget.py
@@ -6,7 +6,7 @@
     EnumCell,
     MsgCell,
     TimeCell,
-    BaseMonitor
+    BaseMonitor, BaseLogWidget
 )
 from ..base import (
     APP_NAME,
@@ -69,7 +69,7 @@ def init_ui(self):
         scroll_area.setWidgetResizable(True)
         scroll_area.setWidget(scroll_widget)
 
-        self.log_monitor = LogMonitor(self.main_engine, self.event_engine)
+        self.log_monitor = LogMonitor2(self.main_engine, self.event_engine)
 
         self.stop_order_monitor = StopOrderMonitor(
             self.main_engine, self.event_engine
@@ -358,6 +358,23 @@ class StopOrderMonitor(BaseMonitor):
     }
 
 
+class LogMonitor2(BaseLogWidget):
+    """
+    Monitor for log data.
+    """
+    __data_keys = [
+        "time",
+        "msg",
+        "level"
+    ]
+    headers = {
+        "time": "时间",
+        "msg": "日志",
+        "level": "LEVEL",
+    }
+
+    event_type = EVENT_CTA_LOG
+
 class LogMonitor(BaseMonitor):
     """
     Monitor for log data.
diff --git a/vnpy/trader/ui/mainwindow.py b/vnpy/trader/ui/mainwindow.py
--- a/vnpy/trader/ui/mainwindow.py
+++ b/vnpy/trader/ui/mainwindow.py
@@ -17,7 +17,7 @@
     TradeMonitor,
     PositionMonitor,
     AccountMonitor,
-    LogMonitor,
+    LogMonitor2 as LogMonitor,
     ActiveOrderMonitor,
     ConnectDialog,
     ContractManager,
diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py
--- a/vnpy/trader/ui/widget.py
+++ b/vnpy/trader/ui/widget.py
@@ -4,9 +4,11 @@
 
 import csv
 import platform
+from datetime import datetime
 from enum import Enum
 from typing import Any, Dict
 from copy import copy
+import html
 from tzlocal import get_localzone
 
 from PyQt5 import QtCore, QtGui, QtWidgets, Qt
@@ -29,7 +31,6 @@
 from ..utility import load_json, save_json, get_digits
 from ..setting import SETTING_FILENAME, SETTINGS
 
-
 COLOR_LONG = QtGui.QColor("red")
 COLOR_SHORT = QtGui.QColor("green")
 COLOR_BID = QtGui.QColor(255, 174, 201)
@@ -184,6 +185,97 @@ def __init__(self, content: str, data: Any):
         self.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
 
 
+class BaseLogWidget(QtWidgets.QTextEdit):
+    headers: Dict[str, str] = {}
+    __show_limit = 1000
+
+    event_type = ""
+    signal: QtCore.pyqtSignal = QtCore.pyqtSignal(Event)
+
+    def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
+        """"""
+        super(BaseLogWidget, self).__init__()
+
+        self.main_engine: MainEngine = main_engine
+        self.event_engine: EventEngine = event_engine
+
+        self.log_data = []
+        headers_html = []
+        for v in self.headers.values():
+            headers_html.append(f"<th>{v}</th>")
+        self.log_html_head = f'<body><table id="log"><tr>{"".join(headers_html)}</tr>'
+        self.log_html_tail = "</table></body>"
+        self.init_ui()
+        self.register_event()
+
+    def init_ui(self):
+        self.setReadOnly(True)
+        self.document().setDocumentMargin(0)
+        self.document().setDefaultStyleSheet("""
+            table {
+                width: 100%;
+                table-layout: fixed;
+                padding-left:0; padding-right:0; padding-top:0; padding-bottom:0;
+            }
+            #log {
+              font-family: 'Microsoft YaHei', 'Lantinghei SC', 'Monaco', 'monospace';
+              border-collapse: collapse;
+              width: 100%;
+              padding-left:0; padding-right:0; padding-top:0; padding-bottom:0;
+            }
+
+            #log td, #log th {
+              border: 3px solid #32414b;
+              white-space: pre-line;
+              padding-left:0; padding-right:0; padding-top:0; padding-bottom:0;
+            }
+
+            #log td {
+              color: #e0e1e4;
+            }
+
+            #log th {
+              text-align: left;
+              background-color: #32414b;
+              color: white;
+            }
+            """)
+
+    def register_event(self) -> None:
+        """
+        Register event handler into event engine.
+        """
+        if self.event_type:
+            self.signal.connect(self.process_event)
+            self.event_engine.register(self.event_type, self.signal.emit)
+
+    def process_event(self, event: Event) -> None:
+        """
+        Process new data from event and update into table.
+        """
+        log_item_html: [str] = []
+        for k in self.headers:
+            c = event.data.__getattribute__(k)
+            if type(c) is datetime:
+                c = c.strftime("%y-%m-%d %H:%M:%S")
+            log_item_html.append(f"<td>{html.escape(str(c))}</td>")
+        self.log_data.append(f"<tr>{''.join(log_item_html)}</tr>")
+        self.check_data()
+        self.set_html_content()
+
+    def check_data(self):
+        if len(self.log_data) >= self.__show_limit:
+            self.log_data.pop(0)
+
+    def set_html_content(self):
+        html = f"{self.log_html_head}{''.join(reversed(self.log_data))}{self.log_html_tail}"
+        self.setHtml(html)
+
+    def setRowCount(self, c: int):
+        self.log_data = self.log_data[:c]
+        self.set_html_content()
+
+
 class BaseMonitor(QtWidgets.QTableWidget):
     """
     Monitor data update in VN Trader.
@@ -393,6 +485,19 @@ class TickMonitor(BaseMonitor):
     }
 
 
+class LogMonitor2(BaseLogWidget):
+    """
+    Monitor for log data.
+    """
+    headers = {
+        "time": "时间",
+        "msg": "日志",
+        "gateway_name": "接口"
+    }
+
+    event_type = EVENT_LOG
+
+
 class LogMonitor(BaseMonitor):
     """
     Monitor for log data.
@@ -746,9 +851,9 @@ def init_ui(self) -> None:
         self.setLayout(vbox)
 
     def create_label(
-        self,
-        color: str = "",
-        alignment: int = QtCore.Qt.AlignLeft
+            self,
+            color: str = "",
+            alignment: int = QtCore.Qt.AlignLeft
     ) -> QtWidgets.QLabel:
         """
         Create label with certain font color.
@@ -941,7 +1046,7 @@ def update_with_cell(self, cell: BaseCell) -> None:
                 direction = Direction.LONG
             elif data.direction == Direction.LONG:
                 direction = Direction.SHORT
-            else:       # Net position mode
+            else:  # Net position mode
                 if data.volume > 0:
                     direction = Direction.SHORT
                 else:


Test patch: