764 lines
21 KiB
Python
764 lines
21 KiB
Python
|
|
# -*- encoding: utf-8 -*-
|
||
|
|
|
||
|
|
import os
|
||
|
|
import copy
|
||
|
|
import math
|
||
|
|
from functools import partial
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
from PyQt6.QtCore import (
|
||
|
|
Qt, QSize, QPoint, QPointF,
|
||
|
|
QMimeData,
|
||
|
|
)
|
||
|
|
from PyQt6.QtWidgets import (
|
||
|
|
QWidget, QTabWidget, QTableWidget, QTableWidgetItem,
|
||
|
|
QVBoxLayout, QMenu, QSizePolicy, QHeaderView,
|
||
|
|
QApplication, QStyledItemDelegate,
|
||
|
|
)
|
||
|
|
from PyQt6.QtGui import (
|
||
|
|
QIcon, QPainter, QColor, QPen, QBrush, QPalette,
|
||
|
|
QFont, QPainterPath, QImage, QAction,
|
||
|
|
QValidator, QIntValidator, QDoubleValidator,
|
||
|
|
)
|
||
|
|
|
||
|
|
from PyQt6.QtCharts import QChartView, QChart, QLineSeries, QScatterSeries, QValueAxis
|
||
|
|
|
||
|
|
from config import config, data_path, script_path
|
||
|
|
import util
|
||
|
|
|
||
|
|
font = QFont("consolas", 14)
|
||
|
|
font.setFamilies(["consolas", "黑体"])
|
||
|
|
|
||
|
|
def str2data(s, delimiter = None, line_end = "\n"):
|
||
|
|
if delimiter is None:
|
||
|
|
delimiter = [",", "\t", ";"]
|
||
|
|
data = s.split(line_end)
|
||
|
|
data = [line for line in data]
|
||
|
|
for i, line in enumerate(data):
|
||
|
|
ss = ""
|
||
|
|
l = []
|
||
|
|
for c in line:
|
||
|
|
if c in delimiter:
|
||
|
|
l.append(ss)
|
||
|
|
ss = ""
|
||
|
|
else:
|
||
|
|
ss += c
|
||
|
|
l.append(ss)
|
||
|
|
data[i] = l
|
||
|
|
return data
|
||
|
|
|
||
|
|
def data2str(data, delimiter = ",", line_end = "\n"):
|
||
|
|
return line_end.join([delimiter.join(line) for line in data])
|
||
|
|
|
||
|
|
class optional_int_validator(QIntValidator):
|
||
|
|
def validate(self, text, pos):
|
||
|
|
if not text:
|
||
|
|
return (QValidator.State.Acceptable, text, pos)
|
||
|
|
return super().validate(text, pos)
|
||
|
|
|
||
|
|
class optional_doublle_validator(QDoubleValidator):
|
||
|
|
def validate(self, text, pos):
|
||
|
|
if not text:
|
||
|
|
return (QValidator.State.Acceptable, text, pos)
|
||
|
|
return super().validate(text, pos)
|
||
|
|
|
||
|
|
def get_step(span):
|
||
|
|
base = math.log10(span)
|
||
|
|
base += 0.1
|
||
|
|
b = int(base)
|
||
|
|
c = int((b - base) * 3)
|
||
|
|
b = 10.0 ** b
|
||
|
|
if c == 0:
|
||
|
|
step = b / 10
|
||
|
|
elif c == 1:
|
||
|
|
step = b / 5
|
||
|
|
else:
|
||
|
|
step = b / 2
|
||
|
|
return step
|
||
|
|
|
||
|
|
def get_tick(a, b):
|
||
|
|
step = get_step(b - a)
|
||
|
|
list_tick = []
|
||
|
|
tick = a - a % step
|
||
|
|
if tick < a:
|
||
|
|
tick += step
|
||
|
|
while tick <= b:
|
||
|
|
list_tick.append(tick)
|
||
|
|
tick += step
|
||
|
|
|
||
|
|
return list_tick
|
||
|
|
|
||
|
|
class header_view(QHeaderView):
|
||
|
|
def __init__(self, *arg, **kwarg):
|
||
|
|
super().__init__(*arg, **kwarg)
|
||
|
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu)
|
||
|
|
|
||
|
|
def contextMenuEvent(self, event):
|
||
|
|
# Get the clicked header position
|
||
|
|
pos = event.pos() # Position relative to the header
|
||
|
|
|
||
|
|
# Get the logical index of the clicked section
|
||
|
|
orient = self.orientation()
|
||
|
|
index = self.logicalIndexAt(pos)
|
||
|
|
parent = self.parent()
|
||
|
|
|
||
|
|
menu_table = QMenu()
|
||
|
|
act_insert_above = QAction("在上方插入行", menu_table, triggered = partial(parent.insert_row, index))
|
||
|
|
act_insert_below = QAction("在下方插入行", menu_table, triggered = partial(parent.insert_row, index + 1))
|
||
|
|
act_remove_row = QAction("删除行", menu_table, triggered = partial(parent.remove_row, index))
|
||
|
|
act_insert_left = QAction("在左侧插入列", menu_table, triggered = partial(parent.insert_col, index))
|
||
|
|
act_insert_right = QAction("在右侧插入列", menu_table, triggered = partial(parent.insert_col, index + 1))
|
||
|
|
act_remove_col = QAction("删除列", menu_table, triggered = partial(parent.remove_col, index))
|
||
|
|
if orient == Qt.Orientation.Horizontal:
|
||
|
|
menu_table.addActions([
|
||
|
|
act_insert_left,
|
||
|
|
act_insert_right,
|
||
|
|
act_remove_col,
|
||
|
|
])
|
||
|
|
else:
|
||
|
|
menu_table.addActions([
|
||
|
|
act_insert_above,
|
||
|
|
act_insert_below,
|
||
|
|
act_remove_row,
|
||
|
|
])
|
||
|
|
menu_table.exec(event.globalPos())
|
||
|
|
|
||
|
|
class table_item(QTableWidgetItem):
|
||
|
|
def __init__(self, text = "", value = None, formatter = None, *arg, **kwarg):
|
||
|
|
super().__init__(*arg, **kwarg)
|
||
|
|
self.data = None
|
||
|
|
self.formatter = formatter
|
||
|
|
if text:
|
||
|
|
self.text = text
|
||
|
|
elif value:
|
||
|
|
self.value = value
|
||
|
|
|
||
|
|
@property
|
||
|
|
def text(self):
|
||
|
|
return super().text()
|
||
|
|
|
||
|
|
@text.setter
|
||
|
|
def text(self, s):
|
||
|
|
if self.formatter:
|
||
|
|
self.data = self.formatter.str2val(s)
|
||
|
|
if self.data is None:
|
||
|
|
s = ""
|
||
|
|
else:
|
||
|
|
s = self.formatter.val2str(self.data)
|
||
|
|
super().setText(s)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def value(self):
|
||
|
|
if not (self.data is None):
|
||
|
|
return self.data
|
||
|
|
if self.formatter:
|
||
|
|
value = self.formatter.str2val(super().text())
|
||
|
|
return value
|
||
|
|
return super().text()
|
||
|
|
|
||
|
|
@value.setter
|
||
|
|
def value(self, v):
|
||
|
|
if v is None:
|
||
|
|
super().setText("")
|
||
|
|
elif self.formatter:
|
||
|
|
super().setText(self.formatter.val2str(v))
|
||
|
|
else:
|
||
|
|
super().setText(str(v))
|
||
|
|
self.data = v
|
||
|
|
|
||
|
|
def empty(self):
|
||
|
|
return self.data is None
|
||
|
|
|
||
|
|
class item_delegate(QStyledItemDelegate):
|
||
|
|
def createEditor(self, parent, option, index):
|
||
|
|
# Create the editor widget (e.g., QLineEdit)
|
||
|
|
editor = super().createEditor(parent, option, index)
|
||
|
|
|
||
|
|
row = index.row()
|
||
|
|
col = index.column()
|
||
|
|
item = self.parent().at(row, col)
|
||
|
|
if item.formatter is None:
|
||
|
|
pass
|
||
|
|
elif item.data is None:
|
||
|
|
item.setText("")
|
||
|
|
else:
|
||
|
|
item.setText(str(item.data))
|
||
|
|
|
||
|
|
if item.formatter:
|
||
|
|
if item.formatter.dtype is int:
|
||
|
|
editor.setValidator(optional_int_validator())
|
||
|
|
if item.formatter.dtype is float:
|
||
|
|
editor.setValidator(optional_doublle_validator())
|
||
|
|
width = self.parent().horizontalHeader().sectionSize(col)
|
||
|
|
editor.setFixedWidth(width)
|
||
|
|
#editor.setMinimumWidth(editor.width())
|
||
|
|
return editor
|
||
|
|
|
||
|
|
def setModelData(self, editor, model, index):
|
||
|
|
# Process data before saving to the model
|
||
|
|
text = editor.text()
|
||
|
|
row = index.row()
|
||
|
|
col = index.column()
|
||
|
|
item = self.parent().at(row, col)
|
||
|
|
if text == "":
|
||
|
|
item.value = None
|
||
|
|
else:
|
||
|
|
if item.formatter:
|
||
|
|
item.value = item.formatter.dtype(text)
|
||
|
|
else:
|
||
|
|
item.text = text
|
||
|
|
|
||
|
|
# def displayText(self, value, locale):
|
||
|
|
# return super().displayText(value, locale)
|
||
|
|
|
||
|
|
# def updateEditorGeometry(self, editor, option, index):
|
||
|
|
# row = index.row()
|
||
|
|
# col = index.column()
|
||
|
|
# item = self.parent().at(row, col)
|
||
|
|
# editor.setWidth(item.width())
|
||
|
|
# editor.setGeometry(option.rect)
|
||
|
|
# # editor.setGeometry(option.rect.adjusted(0, 0, 0, 0))
|
||
|
|
|
||
|
|
class widget_table(QTableWidget):
|
||
|
|
def __init__(self, window, id, config_tab, *arg, **kwarg):
|
||
|
|
super().__init__(*arg, **kwarg)
|
||
|
|
self.window = window
|
||
|
|
self.app = QApplication.instance()
|
||
|
|
horizontal_header = header_view(Qt.Orientation.Horizontal, self)
|
||
|
|
horizontal_header.setMinimumSectionSize(100)
|
||
|
|
self.setHorizontalHeader(horizontal_header)
|
||
|
|
self.setVerticalHeader(header_view(Qt.Orientation.Vertical, self))
|
||
|
|
self.setItemDelegate(item_delegate(self))
|
||
|
|
self.id = id
|
||
|
|
self.config_tab = config_tab
|
||
|
|
self.setFont(font)
|
||
|
|
self.formatter = None
|
||
|
|
|
||
|
|
@property
|
||
|
|
def row(self):
|
||
|
|
return super().rowCount()
|
||
|
|
|
||
|
|
@row.setter
|
||
|
|
def row(self, row):
|
||
|
|
super().setRowCount(row)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def col(self):
|
||
|
|
return super().columnCount()
|
||
|
|
|
||
|
|
@col.setter
|
||
|
|
def col(self, col):
|
||
|
|
super().setColumnCount(col)
|
||
|
|
|
||
|
|
def at(self, row, col):
|
||
|
|
if row >= super().rowCount():
|
||
|
|
self.setRowCount(row + 1)
|
||
|
|
if col >= super().columnCount():
|
||
|
|
self.setColumnCount(col + 1)
|
||
|
|
item = super().item(row, col)
|
||
|
|
if item is None:
|
||
|
|
formatter = None
|
||
|
|
if self.formatter:
|
||
|
|
formatter = self.formatter(row, col)
|
||
|
|
item = table_item(formatter = formatter)
|
||
|
|
super().setItem(row, col, item)
|
||
|
|
return item
|
||
|
|
|
||
|
|
def clear_content(self):
|
||
|
|
super().clearContents()
|
||
|
|
|
||
|
|
def load_data(self, data, row_begin = 0, col_begin = 0, row_end = None, col_end = None, as_value = False):
|
||
|
|
row = row_begin
|
||
|
|
while (not row_end or row < row_end) and row < len(data):
|
||
|
|
line = data[row]
|
||
|
|
col = col_begin
|
||
|
|
while (not col_end or col < col_end) and col < len(line):
|
||
|
|
if as_value:
|
||
|
|
self.at(row, col).value = line[col]
|
||
|
|
else:
|
||
|
|
self.at(row, col).text = line[col]
|
||
|
|
col += 1
|
||
|
|
row += 1
|
||
|
|
|
||
|
|
def dump_data(self, row_begin = 0, col_begin = 0, row_end = None, col_end = None, as_value = False):
|
||
|
|
if row_end is None:
|
||
|
|
row_end = self.row
|
||
|
|
if col_end is None:
|
||
|
|
col_end = self.col
|
||
|
|
if row_end <= row_begin or col_end <= col_begin:
|
||
|
|
return []
|
||
|
|
|
||
|
|
data = []
|
||
|
|
for row in range(row_begin, row_end):
|
||
|
|
line = []
|
||
|
|
for col in range(col_begin, col_end):
|
||
|
|
if as_value:
|
||
|
|
line.append(self.at(row, col).value)
|
||
|
|
else:
|
||
|
|
line.append(self.at(row, col).text)
|
||
|
|
data.append(line)
|
||
|
|
|
||
|
|
return data
|
||
|
|
|
||
|
|
def load_str(self, s, delimiter = None, line_end = "\n", *arg, **kwarg):
|
||
|
|
data = str2data(s, delimiter, line_end)
|
||
|
|
self.load_data(data, *arg, **kwarg)
|
||
|
|
|
||
|
|
def dump_str(self, delimiter = ",", line_end = "\n", *arg, **kwarg):
|
||
|
|
data = self.dump_data(*arg, **kwarg)
|
||
|
|
s = data2str(data, delimiter, line_end)
|
||
|
|
return s
|
||
|
|
|
||
|
|
def load_file(self, filename, *arg, **kwarg):
|
||
|
|
with open(filename, "r", encoding="utf-8") as fobj:
|
||
|
|
content = fobj.read()
|
||
|
|
self.load_str(content, *arg, **kwarg)
|
||
|
|
|
||
|
|
def dump_file(self, filename, *arg, **kwarg):
|
||
|
|
s = self.dump_str(*arg, **kwarg)
|
||
|
|
with open(filename, "w", encoding="utf-8") as fobj:
|
||
|
|
fobj.write(s)
|
||
|
|
|
||
|
|
def initial(self):
|
||
|
|
config_tab = self.config_tab
|
||
|
|
if "row" in config_tab:
|
||
|
|
self.row = config_tab["row"]
|
||
|
|
if "col" in config_tab:
|
||
|
|
self.col = config_tab["col"]
|
||
|
|
if "column_header" in config_tab:
|
||
|
|
self.setHorizontalHeaderLabels(config_tab["column_header"])
|
||
|
|
if "format_script" in config_tab:
|
||
|
|
filename = os.path.join(script_path, config_tab["format_script"])
|
||
|
|
module = util.load_module(filename)
|
||
|
|
self.formatter = module.table_formatter
|
||
|
|
if "data" in config_tab:
|
||
|
|
data = config_tab["data"]
|
||
|
|
self.load_data(data)
|
||
|
|
if "datafile" in config_tab:
|
||
|
|
datafile = os.path.join(data_path, config_tab["datafile"])
|
||
|
|
self.load_file(datafile)
|
||
|
|
if "span" in config_tab:
|
||
|
|
for span in config_tab["span"]:
|
||
|
|
self.setSpan(*span)
|
||
|
|
self.fit_content()
|
||
|
|
|
||
|
|
def reset_content(self):
|
||
|
|
self.clear_content()
|
||
|
|
self.initial()
|
||
|
|
|
||
|
|
def fit_content(self):
|
||
|
|
header = self.horizontalHeader()
|
||
|
|
header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
|
||
|
|
|
||
|
|
def calc_index(self):
|
||
|
|
list_index = self.selectedIndexes()
|
||
|
|
if not list_index:
|
||
|
|
return
|
||
|
|
min_r = min(list_index, key = lambda x:x.row()).row()
|
||
|
|
max_r = max(list_index, key = lambda x:x.row()).row() + 1
|
||
|
|
min_c = min(list_index, key = lambda x:x.column()).column()
|
||
|
|
max_c = max(list_index, key = lambda x:x.column()).column() + 1
|
||
|
|
flag_rect = (max_r - min_r) * (max_c - min_c) != len(list_index)
|
||
|
|
flag_row = flag_rect and min_c == 0 and max_c == self.columnCount()
|
||
|
|
flag_col = flag_rect and min_r == 0 and max_r == self.rowCount()
|
||
|
|
return list_index, min_r, max_r, min_c, max_c, flag_rect, flag_row, flag_col
|
||
|
|
|
||
|
|
def load_clipboard(self):
|
||
|
|
clipboard = self.app.clipboard()
|
||
|
|
mimedata = clipboard.mimeData()
|
||
|
|
|
||
|
|
# if mimedata.hasHtml():
|
||
|
|
# # Handle HTML with explicit UTF-16 decoding
|
||
|
|
# raw_html = bytes(mimedata.data("text/html"))
|
||
|
|
# try:
|
||
|
|
# html = raw_html.decode("utf-16")
|
||
|
|
# except UnicodeDecodeError:
|
||
|
|
# html = raw_html.decode("utf-8", errors="replace")
|
||
|
|
# self.parse_html_table(html)
|
||
|
|
|
||
|
|
text = None
|
||
|
|
if mimedata.hasText():
|
||
|
|
# Handle plain text with encoding detection
|
||
|
|
raw_text = bytes(mimedata.data("text/plain"))
|
||
|
|
|
||
|
|
# Check for UTF-16 BOM
|
||
|
|
if raw_text.startswith(b"\xff\xfe") or raw_text.startswith(b"\xfe\xff"):
|
||
|
|
text = raw_text.decode("utf-16")
|
||
|
|
else:
|
||
|
|
try:
|
||
|
|
text = raw_text.decode("utf-8")
|
||
|
|
except UnicodeDecodeError:
|
||
|
|
text = raw_text.decode(locale.getpreferredencoding(), errors="replace")
|
||
|
|
if text is None:
|
||
|
|
return
|
||
|
|
|
||
|
|
data = str2data(text, delimiter=["\t"])
|
||
|
|
return data
|
||
|
|
|
||
|
|
def clear_select(self):
|
||
|
|
list_index = self.selectedIndexes()
|
||
|
|
for index in list_index:
|
||
|
|
row = index.row()
|
||
|
|
col = index.column()
|
||
|
|
self.at(row, col).value = None
|
||
|
|
|
||
|
|
def copy_select(self, as_value = False):
|
||
|
|
data = self.calc_index()
|
||
|
|
if not data:
|
||
|
|
return
|
||
|
|
list_index, min_r, max_r, min_c, max_c, flag_rect, flag_row, flag_col = data
|
||
|
|
|
||
|
|
data = [[""] * (max_c - min_c) for row in range(max_r - min_r)]
|
||
|
|
for index in list_index:
|
||
|
|
row = index.row()
|
||
|
|
col = index.column()
|
||
|
|
item = self.at(row, col)
|
||
|
|
if as_value:
|
||
|
|
if item.value is None:
|
||
|
|
data[row - min_r][col - min_c] = ""
|
||
|
|
else:
|
||
|
|
data[row - min_r][col - min_c] = str(self.at(row, col).value)
|
||
|
|
else:
|
||
|
|
data[row - min_r][col - min_c] = self.at(row, col).text
|
||
|
|
|
||
|
|
text = data2str(data, delimiter = "\t")
|
||
|
|
clipboard = self.app.clipboard()
|
||
|
|
mimedata = QMimeData()
|
||
|
|
mimedata.setText(text)
|
||
|
|
clipboard.setMimeData(mimedata)
|
||
|
|
|
||
|
|
def paste_select(self, as_value = False):
|
||
|
|
data = self.calc_index()
|
||
|
|
if not data:
|
||
|
|
return
|
||
|
|
list_index, min_r, max_r, min_c, max_c, flag_rect, flag_row, flag_col = data
|
||
|
|
|
||
|
|
data = self.load_clipboard()
|
||
|
|
|
||
|
|
for index in list_index:
|
||
|
|
row = index.row()
|
||
|
|
col = index.column()
|
||
|
|
r = row - min_r
|
||
|
|
if r >= len(data):
|
||
|
|
continue
|
||
|
|
c = col - min_c
|
||
|
|
line = data[r]
|
||
|
|
if c >= len(line):
|
||
|
|
continue
|
||
|
|
item = self.at(row, col)
|
||
|
|
if as_value and item.formatter:
|
||
|
|
if line[c] == "":
|
||
|
|
item.value = None
|
||
|
|
else:
|
||
|
|
dtype = item.formatter.dtype
|
||
|
|
try:
|
||
|
|
try:
|
||
|
|
item.value = dtype(line[c])
|
||
|
|
except:
|
||
|
|
if dtype is int:
|
||
|
|
item.value = int(float(line[c]))
|
||
|
|
except:
|
||
|
|
raise RuntimeError(f"无法将\"{line[c]}\"转化为{dype}类型")
|
||
|
|
else:
|
||
|
|
item.text = line[c]
|
||
|
|
|
||
|
|
def insert_row(self, row):
|
||
|
|
self.insertRow(row)
|
||
|
|
|
||
|
|
def remove_row(self, row):
|
||
|
|
self.removeRow(row)
|
||
|
|
|
||
|
|
def insert_col(self, col):
|
||
|
|
self.insertColumn(col)
|
||
|
|
|
||
|
|
def remove_col(self, col):
|
||
|
|
self.removeColumn(col)
|
||
|
|
|
||
|
|
def contextMenuEvent(self, event):
|
||
|
|
data = self.calc_index()
|
||
|
|
if not data:
|
||
|
|
return
|
||
|
|
list_index, min_r, max_r, min_c, max_c, flag_rect, flag_row, flag_col = data
|
||
|
|
|
||
|
|
menu_table = QMenu()
|
||
|
|
act_clear_select = QAction("清空所选区域", menu_table, triggered = self.clear_select)
|
||
|
|
act_copy_text = QAction("复制所选区域文本", menu_table, triggered = self.copy_select)
|
||
|
|
act_copy_value = QAction("复制所选区域数据", menu_table, triggered = partial(self.copy_select, True))
|
||
|
|
act_paste_text = QAction("粘贴文本到所选区域", menu_table, triggered = self.paste_select)
|
||
|
|
act_paste_value = QAction("粘贴数据到所选区域", menu_table, triggered = partial(self.paste_select, True))
|
||
|
|
menu_table.addActions([
|
||
|
|
act_clear_select,
|
||
|
|
act_copy_text,
|
||
|
|
act_copy_value,
|
||
|
|
act_paste_text,
|
||
|
|
act_paste_value,
|
||
|
|
])
|
||
|
|
menu_table.exec(event.globalPos())
|
||
|
|
|
||
|
|
def keyPressEvent(self, event):
|
||
|
|
if (
|
||
|
|
event.key() == Qt.Key.Key_Delete
|
||
|
|
):
|
||
|
|
self.clear_select()
|
||
|
|
event.accept()
|
||
|
|
elif (
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||
|
|
and event.key() == Qt.Key.Key_C
|
||
|
|
):
|
||
|
|
self.copy_select()
|
||
|
|
event.accept()
|
||
|
|
elif (
|
||
|
|
(event.modifiers() == Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier)
|
||
|
|
and event.key() == Qt.Key.Key_C
|
||
|
|
):
|
||
|
|
self.copy_select(as_value = True)
|
||
|
|
event.accept()
|
||
|
|
elif (
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||
|
|
and event.key() == Qt.Key.Key_V
|
||
|
|
):
|
||
|
|
self.paste_select()
|
||
|
|
event.accept()
|
||
|
|
elif (
|
||
|
|
(event.modifiers() == Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier)
|
||
|
|
and event.key() == Qt.Key.Key_V
|
||
|
|
):
|
||
|
|
self.paste_select(as_value = True)
|
||
|
|
event.accept()
|
||
|
|
else:
|
||
|
|
super().keyPressEvent(event)
|
||
|
|
|
||
|
|
class chart_plot(QChart):
|
||
|
|
def __init__(self, window, config_tab, *arg):
|
||
|
|
super().__init__(*arg)
|
||
|
|
self.window = window
|
||
|
|
|
||
|
|
self.setBackgroundBrush(QBrush(QColor(0, 0, 0)))
|
||
|
|
self.setAnimationOptions(QChart.AnimationOption.SeriesAnimations)
|
||
|
|
self.legend().setLabelColor(QColor(220, 220, 220))
|
||
|
|
|
||
|
|
self.axis_x = QValueAxis()
|
||
|
|
self.axis_y = QValueAxis()
|
||
|
|
|
||
|
|
if "axis_x_title" in config_tab:
|
||
|
|
title = config_tab["axis_x_title"]
|
||
|
|
self.axis_x.setTitleText(title)
|
||
|
|
if "axis_y_title" in config_tab:
|
||
|
|
title = config_tab["axis_y_title"]
|
||
|
|
self.axis_y.setTitleText(title)
|
||
|
|
|
||
|
|
axis_list = [self.axis_x, self.axis_y]
|
||
|
|
for axis in axis_list:
|
||
|
|
axis.setTitleBrush(QBrush(QColor(220, 220, 220)))
|
||
|
|
axis.setLabelsColor(QColor(220, 220, 220))
|
||
|
|
axis.setLinePenColor(QColor(200, 200, 200))
|
||
|
|
axis.setGridLineColor(QColor(100, 100, 100))
|
||
|
|
|
||
|
|
self.addAxis(self.axis_x, Qt.AlignmentFlag.AlignBottom)
|
||
|
|
self.addAxis(self.axis_y, Qt.AlignmentFlag.AlignLeft)
|
||
|
|
|
||
|
|
self.list_name = []
|
||
|
|
|
||
|
|
def plot(self, *arg, **kwarg):
|
||
|
|
self.removeAllSeries()
|
||
|
|
self.list_name.clear()
|
||
|
|
|
||
|
|
min_x, max_x = None, None
|
||
|
|
min_y, max_y = None, None
|
||
|
|
|
||
|
|
for i in range((len(arg) + 2) // 3):
|
||
|
|
x = arg[i * 3]
|
||
|
|
y = arg[i * 3 + 1]
|
||
|
|
line = QLineSeries()
|
||
|
|
if i * 3 + 2 < len(arg):
|
||
|
|
name = arg[i * 3 + 2]
|
||
|
|
else:
|
||
|
|
name = f"数据{i}"
|
||
|
|
line.setName(name)
|
||
|
|
self.list_name.append(name)
|
||
|
|
self.addSeries(line)
|
||
|
|
|
||
|
|
scatter = QScatterSeries()
|
||
|
|
scatter.setColor(line.color())
|
||
|
|
scatter.setMarkerSize(10)
|
||
|
|
scatter.setPointLabelsColor(line.color())
|
||
|
|
#scatter.setPointLabelsVisible()
|
||
|
|
self.addSeries(scatter)
|
||
|
|
|
||
|
|
line.attachAxis(self.axis_x)
|
||
|
|
line.attachAxis(self.axis_y)
|
||
|
|
scatter.attachAxis(self.axis_x)
|
||
|
|
scatter.attachAxis(self.axis_y)
|
||
|
|
|
||
|
|
marker = self.legend().markers(scatter)
|
||
|
|
for i in marker:
|
||
|
|
i.setVisible(False)
|
||
|
|
|
||
|
|
for i in range(len(x)):
|
||
|
|
xx = x[i]
|
||
|
|
yy = y[i]
|
||
|
|
point = QPointF(xx, yy)
|
||
|
|
line.append(point)
|
||
|
|
scatter.append(point)
|
||
|
|
|
||
|
|
# Calculate and set axis ranges
|
||
|
|
x1, x2 = min(x), max(x)
|
||
|
|
y1, y2 = min(y), max(y)
|
||
|
|
if min_x is None:
|
||
|
|
min_x, max_x = x1, x2
|
||
|
|
min_y, max_y = y1, y2
|
||
|
|
else:
|
||
|
|
min_x, max_x = min(min_x, x1), max(max_x, x2)
|
||
|
|
min_y, max_y = min(min_y, y1), max(max_y, y2)
|
||
|
|
|
||
|
|
# Add 10% padding to axis ranges
|
||
|
|
|
||
|
|
min_x, max_x = min_x - 0.1 * (max_x - min_x), max_x + 0.1 * (max_x - min_x)
|
||
|
|
min_y, max_y = min_y - 0.1 * (max_y - min_y), max_y + 0.1 * (max_y - min_y)
|
||
|
|
|
||
|
|
self.axis_x.setRange(min_x, max_x)
|
||
|
|
self.axis_y.setRange(min_y, max_y)
|
||
|
|
|
||
|
|
step = get_step(max_x - min_x)
|
||
|
|
self.axis_x.setTickType(QValueAxis.TickType.TicksDynamic);
|
||
|
|
self.axis_x.setTickAnchor(min_x - min_x % step);
|
||
|
|
self.axis_x.setTickInterval(step);
|
||
|
|
step = get_step(max_y - min_y)
|
||
|
|
self.axis_y.setTickType(QValueAxis.TickType.TicksDynamic);
|
||
|
|
self.axis_y.setTickAnchor(min_y - min_y % step);
|
||
|
|
self.axis_y.setTickInterval(step);
|
||
|
|
|
||
|
|
def contextMenuEvent(self, event):
|
||
|
|
if not self.list_name:
|
||
|
|
return
|
||
|
|
menu = QMenu()
|
||
|
|
for index, name in enumerate(self.list_name):
|
||
|
|
act_toggle_series = QAction(f"显示/隐藏{name}", menu, triggered = partial(self.toggle_series, index))
|
||
|
|
menu.addAction(act_toggle_series)
|
||
|
|
menu.exec(event.screenPos())
|
||
|
|
|
||
|
|
def toggle_series(self, index):
|
||
|
|
line = self.series()[index * 2]
|
||
|
|
scatter = self.series()[index * 2 + 1]
|
||
|
|
flag = line.isVisible()
|
||
|
|
flag = not flag
|
||
|
|
line.setVisible(flag)
|
||
|
|
scatter.setVisible(flag)
|
||
|
|
marker = self.legend().markers(scatter)
|
||
|
|
for i in marker:
|
||
|
|
i.setVisible(False)
|
||
|
|
|
||
|
|
class chart_view(QChartView):
|
||
|
|
def __init__(self, window, id, config_tab, *arg):
|
||
|
|
super().__init__(*arg)
|
||
|
|
self.window = window
|
||
|
|
self.id = id
|
||
|
|
self.config_tab = config_tab
|
||
|
|
|
||
|
|
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||
|
|
self.chart_plot = chart_plot(window, config_tab)
|
||
|
|
self.setChart(self.chart_plot)
|
||
|
|
|
||
|
|
def plot(self, *arg, **kwarg):
|
||
|
|
self.chart_plot.plot(*arg, **kwarg)
|
||
|
|
|
||
|
|
class tab_widget(QTabWidget):
|
||
|
|
def __init__(self, window, *arg, **kwarg):
|
||
|
|
super().__init__(*arg, **kwarg)
|
||
|
|
self.window = window
|
||
|
|
self.currentChanged.connect(self.on_current_changed)
|
||
|
|
self.setFont(font)
|
||
|
|
|
||
|
|
def on_current_changed(self, index):
|
||
|
|
list_id = self.window.widget_display.list_id
|
||
|
|
if index < 0 or not list_id:
|
||
|
|
return
|
||
|
|
table_id = list_id[index]
|
||
|
|
self.window.widget_interact.load_interact(table_id)
|
||
|
|
|
||
|
|
class widget_display(QWidget):
|
||
|
|
def __init__(self, window, *arg, **kwarg):
|
||
|
|
super().__init__(*arg, **kwarg)
|
||
|
|
self.window = window
|
||
|
|
|
||
|
|
self.setFont(font)
|
||
|
|
|
||
|
|
layout_base = QVBoxLayout()
|
||
|
|
self.setLayout(layout_base)
|
||
|
|
self.tab_widget = tab_widget(window)
|
||
|
|
layout_base.addWidget(self.tab_widget)
|
||
|
|
|
||
|
|
self.map_table = {}
|
||
|
|
self.map_graph = {}
|
||
|
|
self.list_id = []
|
||
|
|
|
||
|
|
if not ("display" in config):
|
||
|
|
return
|
||
|
|
config_display = config["display"]
|
||
|
|
for project, config_project in config_display.items():
|
||
|
|
for config_tab in config_project:
|
||
|
|
if not "type" in config_tab:
|
||
|
|
continue
|
||
|
|
if config_tab["type"] == "table":
|
||
|
|
id = config_tab["id"]
|
||
|
|
label = config_tab["label"]
|
||
|
|
widget = widget_table(self.window, id, config_tab)
|
||
|
|
self.map_table[id] = widget
|
||
|
|
elif config_tab["type"] == "graph":
|
||
|
|
id = config_tab["id"]
|
||
|
|
label = config_tab["label"]
|
||
|
|
widget = chart_view(self.window, id, config_tab)
|
||
|
|
self.map_graph[id] = widget
|
||
|
|
|
||
|
|
def initial_table(self):
|
||
|
|
for widget in self.map_table.values():
|
||
|
|
widget.initial()
|
||
|
|
|
||
|
|
def load_project(self, project):
|
||
|
|
self.list_id.clear()
|
||
|
|
self.tab_widget.clear()
|
||
|
|
if not ("display" in config):
|
||
|
|
return
|
||
|
|
|
||
|
|
config_display = config["display"]
|
||
|
|
if not (project in config_display):
|
||
|
|
return
|
||
|
|
config_project = config_display[project]
|
||
|
|
for config_tab in config_project:
|
||
|
|
if not "type" in config_tab:
|
||
|
|
continue
|
||
|
|
id = config_tab["id"]
|
||
|
|
label = config_tab["label"]
|
||
|
|
widget = None
|
||
|
|
if config_tab["type"] == "table":
|
||
|
|
widget = self.map_table[id]
|
||
|
|
elif config_tab["type"] == "graph":
|
||
|
|
widget = self.map_graph[id]
|
||
|
|
self.list_id.append(id)
|
||
|
|
self.tab_widget.addTab(widget, label)
|
||
|
|
self.tab_widget.setTabToolTip(self.tab_widget.count() - 1, f"id:{id}")
|
||
|
|
|
||
|
|
def get_current_widget(self):
|
||
|
|
widget = self.tab_widget.currentWidget()
|
||
|
|
return widget
|
||
|
|
|
||
|
|
def get_current_id(self):
|
||
|
|
widget = self.get_current_widget()
|
||
|
|
if widget is None:
|
||
|
|
return
|
||
|
|
return self.get_current_widget().id
|
||
|
|
|
||
|
|
def load_table(self, data):
|
||
|
|
widget = self.get_current_widget()
|
||
|
|
if widget is None:
|
||
|
|
return
|
||
|
|
widget.load_table(data)
|
||
|
|
|
||
|
|
def dump_table(self):
|
||
|
|
widget = self.get_current_widget()
|
||
|
|
if widget is None:
|
||
|
|
return
|
||
|
|
return widget.dump_table()
|
||
|
|
|
||
|
|
|