668 lines
27 KiB
Python
668 lines
27 KiB
Python
# -*- coding: utf-8 -*-
|
||
import sys, os, cv2
|
||
from os import makedirs
|
||
from os.path import join, exists
|
||
from loguru import logger
|
||
from json import loads
|
||
from ruamel.yaml import safe_load, YAML
|
||
import random, sys, math, inspect, psutil
|
||
from pathlib import Path
|
||
from PySide6.QtGui import QIcon, QColor
|
||
from PySide6.QtCore import QObject, QRectF, QEventLoop, QIODevice, QTextStream, QFile
|
||
from PySide6.QtWidgets import QApplication
|
||
|
||
import DrGraph.utils.vclEnums as enums
|
||
#region Property
|
||
class Property:
|
||
def __init__(self, read_func=None, write_func=None, default=None, hasMember=True):
|
||
self.read_func = read_func
|
||
self.write_func = write_func
|
||
self.default = default
|
||
self.owner_class = None
|
||
self.private_name = None
|
||
self.hasMember = hasMember
|
||
def __set_name__(self, owner, name):
|
||
if self.hasMember:
|
||
self.private_name = f"_{name}"
|
||
self.owner_class = owner
|
||
def callDirectGet(self, instance, owner):
|
||
if instance is None or not self.hasMember:
|
||
return self.default
|
||
if not hasattr(instance, self.private_name):
|
||
return self.default
|
||
return getattr(instance, self.private_name)
|
||
def callCustomGet(self, instance, owner):
|
||
if instance is None:
|
||
return self
|
||
|
||
if self.read_func is None:
|
||
if hasattr(instance, self.private_name):
|
||
return getattr(instance, self.private_name)
|
||
return self.default
|
||
|
||
try:
|
||
if isinstance(self.read_func, str):
|
||
if hasattr(instance, self.read_func):
|
||
method = getattr(instance, self.read_func)
|
||
return method()
|
||
elif hasattr(self.read_func, '__name__'):
|
||
method_name = self.read_func.__name__
|
||
if hasattr(instance, method_name):
|
||
method = getattr(instance, method_name)
|
||
return method()
|
||
elif callable(self.read_func):
|
||
try:
|
||
return self.read_func(instance)
|
||
except (TypeError, AttributeError):
|
||
return self.read_func()
|
||
except Exception as e:
|
||
logger.error(e)
|
||
|
||
if hasattr(instance, self.private_name):
|
||
return getattr(instance, self.private_name)
|
||
return self.default
|
||
|
||
def callDirectSet(self, instance, value):
|
||
if instance is None or not self.hasMember:
|
||
return
|
||
setattr(instance, self.private_name, value)
|
||
|
||
def callCustomSet(self, instance, value):
|
||
try:
|
||
if isinstance(self.write_func, str):
|
||
if hasattr(instance, self.write_func):
|
||
method = getattr(instance, self.write_func)
|
||
method(value)
|
||
elif hasattr(self.write_func, '__name__'):
|
||
method_name = self.write_func.__name__
|
||
if hasattr(instance, method_name):
|
||
method = getattr(instance, method_name)
|
||
method(value)
|
||
elif callable(self.write_func):
|
||
try:
|
||
self.write_func(instance, value)
|
||
except (TypeError, AttributeError):
|
||
self.write_func(value)
|
||
except Exception as e:
|
||
pass
|
||
|
||
class Property_rw(Property):
|
||
def __init__(self, default=None, hasMember=True):
|
||
super().__init__(None, None, default, hasMember)
|
||
def __get__(self, instance, owner):
|
||
return self.callDirectGet(instance, owner)
|
||
def __set__(self, instance, value):
|
||
self.callDirectSet(instance, value)
|
||
|
||
class Property_Rw(Property):
|
||
def __init__(self, read_func=None, default=None, hasMember=True):
|
||
super().__init__(read_func, None, default, hasMember)
|
||
|
||
def __get__(self, instance, owner):
|
||
return self.callCustomGet(instance, owner)
|
||
|
||
def __set__(self, instance, value):
|
||
setattr(instance, self.private_name, value)
|
||
|
||
class Property_rW(Property):
|
||
def __init__(self, write_func=None, default=None, hasMember=True):
|
||
super().__init__(None, write_func, default, hasMember)
|
||
|
||
def __get__(self, instance, owner):
|
||
return self.callDirectGet(instance, owner)
|
||
|
||
def __set__(self, instance, value):
|
||
self.callCustomSet(instance, value)
|
||
|
||
class Property_RW(Property):
|
||
def __init__(self, read_func=None, write_func=None, default=None, hasMember=True):
|
||
super().__init__(read_func, write_func, default, hasMember)
|
||
|
||
def __get__(self, instance, owner):
|
||
return self.callCustomGet(instance, owner)
|
||
|
||
def __set__(self, instance, value):
|
||
self.callCustomSet(instance, value)
|
||
#endregion Property
|
||
|
||
class AppHelper(QObject):
|
||
app = Property_rw(None)
|
||
def setBriefStatusText(self, text):
|
||
if self.briefStatusControl:
|
||
self.briefStatusControl.setText(text)
|
||
else:
|
||
print(text)
|
||
briefStatusText = Property_rW(setBriefStatusText, '')
|
||
|
||
def setProgress(self, value):
|
||
if self.progressBarControl:
|
||
self.progressBarControl.setValue(value)
|
||
progress = Property_rW(setProgress, 0)
|
||
|
||
def setProgressMax(self, value):
|
||
if self.progressBarControl:
|
||
self.progressBarControl.setMaximum(value)
|
||
progressMax = Property_rW(setProgressMax, 100)
|
||
|
||
def setProgressMin(self, value):
|
||
if self.progressBarControl:
|
||
self.progressBarControl.setMinimum(value)
|
||
progressMin = Property_rW(setProgressMin, 0)
|
||
|
||
def __init__(self):
|
||
self.briefStatusControl = None
|
||
self.progressBarControl = None
|
||
self._briefStatusText = ''
|
||
pass
|
||
|
||
class Helper:
|
||
OnLogMsg = None
|
||
AppFlag_SaveAnalysisResult = True
|
||
AppFlag_SaveLog = False
|
||
App = None
|
||
|
||
@staticmethod
|
||
def castRange(value, minValue, maxValue):
|
||
return max(minValue, min(maxValue, value))
|
||
# 取得程序目录
|
||
@staticmethod
|
||
def getPath_App():
|
||
if getattr(sys, 'frozen', False):
|
||
# 如果程序是打包的exe文件
|
||
return os.path.dirname(sys.executable)
|
||
else:
|
||
# 如果是Python脚本 - 获取上两级目录
|
||
current_file = os.path.abspath(__file__) # f:\PySide6\AiBase\DrGraph\utils\Helper.py
|
||
current_dir = os.path.dirname(current_file) # f:\PySide6\AiBase\DrGraph\utils
|
||
parent_dir = os.path.dirname(current_dir) # f:\PySide6\AiBase\DrGraph
|
||
root_dir = os.path.dirname(parent_dir) # f:\PySide6\AiBase
|
||
return root_dir
|
||
|
||
@staticmethod
|
||
def fitOS(file_name):
|
||
if sys.platform.startswith('win'):
|
||
file_name = file_name.replace('/','\\')
|
||
else:
|
||
file_name = file_name.replace('\\', '/')
|
||
return file_name
|
||
|
||
def generateDistinctColors(n, s=0.8, v=0.7):
|
||
import colorsys
|
||
colors = []
|
||
for i in range(n):
|
||
hue = i * 1.0 / n # 均匀分布在 [0, 1)
|
||
r, g, b = colorsys.hsv_to_rgb(hue, s, v)
|
||
colors.append(QColor(r * 255, g * 255, b * 255))
|
||
return colors
|
||
|
||
def setBriefStatusText(self, text):
|
||
Helper.App.setBriefStatusText(text)
|
||
briefStatusText = Property_rW(setBriefStatusText, '')
|
||
@staticmethod
|
||
def Sleep(msec):
|
||
QApplication.processEvents(QEventLoop.AllEvents, msec)
|
||
|
||
|
||
@staticmethod
|
||
def getAbsoluteFileName(file_name):
|
||
if os.path.isabs(file_name):
|
||
return Helper.fitOS(file_name)
|
||
else:
|
||
return Helper.fitOS(os.path.join(Helper.getPath_App(), file_name))
|
||
@staticmethod
|
||
def getConfigs(path, read_type='yml'):
|
||
"""
|
||
读取配置文件并返回解析后的配置信息
|
||
|
||
:param path: 配置文件路径
|
||
:param read_type: 配置文件类型,默认为'yml',可选'json'或'yml'
|
||
:return: 解析后的配置信息,JSON格式返回字典,YML格式返回对应的数据结构
|
||
:raises Exception: 当无法获取配置信息时抛出异常
|
||
"""
|
||
yaml = YAML(typ='safe', pure=True)
|
||
with open(path, 'r', encoding='utf-8') as f:
|
||
return yaml.load(f)
|
||
# with open(path, 'r', encoding="utf-8") as f:
|
||
# # 根据文件类型选择相应的解析方式
|
||
# if read_type == 'json':
|
||
# return loads(f.read())
|
||
# if read_type == 'yml':
|
||
# return safe_load(f)
|
||
# 如果未成功读取配置信息,则抛出异常
|
||
raise Exception('路径: %s未获取配置信息' % path)
|
||
|
||
@staticmethod
|
||
def getTooltipText(content):
|
||
# 增加一个小喇叭图标
|
||
# content = f'<img src="appIOs/res/images/icons/info.png" width="16" height="16"> {content}'
|
||
return f"""
|
||
<html>
|
||
<head/><body>
|
||
<p><span style=" font-weight:600; color:#ffffff;">DrGraph <img src="appIOs/res/images/icons/Notice.png" width="16" height="16"></span></p>
|
||
<p>{content}</p>
|
||
</body>
|
||
</html>
|
||
"""
|
||
@staticmethod
|
||
def log_init(app, base_dir, env):
|
||
"""
|
||
初始化日志配置
|
||
|
||
:param base_dir: 基础目录路径,用于定位配置文件和日志文件存储位置
|
||
:param env: 环境标识,用于加载对应环境的日志配置文件
|
||
:return: 无返回值
|
||
"""
|
||
Helper.App = AppHelper()
|
||
Helper.App.app = app
|
||
# QToolTip样式 - 自定义样式 - 增加Header
|
||
app.setStyleSheet("""
|
||
QToolTip {
|
||
background-color: #dd2222;
|
||
color: #f0f0f0;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
padding: 6px;
|
||
font: 10pt "Segoe UI";
|
||
opacity: 220;
|
||
}
|
||
""")
|
||
|
||
log_config = Helper.getConfigs(join(base_dir, 'appIOs/configs/logger/drgraph_%s_logger.yml' % env))
|
||
# 判断日志文件是否存在,不存在创建
|
||
base_path = join(base_dir, log_config.get("base_path"))
|
||
if not exists(base_path):
|
||
makedirs(base_path)
|
||
# 移除日志设置
|
||
logger.remove(handler_id=None)
|
||
# 打印日志到文件
|
||
if bool(log_config.get("enable_file_log")):
|
||
logger.add(join(base_path, log_config.get("log_name")),
|
||
rotation=log_config.get("rotation"),
|
||
retention=log_config.get("retention"),
|
||
format=log_config.get("log_fmt"),
|
||
level=log_config.get("level"),
|
||
enqueue=True,
|
||
encoding=log_config.get("encoding"))
|
||
# 控制台输出
|
||
if bool(log_config.get("enable_stderr")):
|
||
logger.add(sys.stderr,
|
||
format=log_config.get("log_fmt"),
|
||
level=log_config.get("level"),
|
||
enqueue=True)
|
||
logger.info("\n\n\n----=========== 日志配置初始化完成, 开始新的日志记录 ==========----")
|
||
|
||
@staticmethod
|
||
def log_info(msg, toWss = False):
|
||
if Helper.OnLogMsg:
|
||
Helper.OnLogMsg(f'INFO: {msg}', 'black')
|
||
caller = inspect.stack()[1]
|
||
logger.info(f"[{Path(caller.filename).name}:{caller.lineno}.{caller.function}()] > {msg}")
|
||
if toWss:
|
||
Helper.log_wss({"type": "log", "kind": "INFO", "msg" : msg} )
|
||
@staticmethod
|
||
def log_error(msg, toWss = False):
|
||
if Helper.OnLogMsg:
|
||
Helper.OnLogMsg(f'ERROR: {msg}', 'red')
|
||
caller = inspect.stack()[1]
|
||
logger.error(f"[{Path(caller.filename).name}:{caller.lineno}.{caller.function}()] > {msg}")
|
||
if toWss:
|
||
Helper.log_wss({"type": "log", "kind": "ERROR", "msg" : msg} )
|
||
@staticmethod
|
||
def log_warning(msg, toWss = False):
|
||
if Helper.OnLogMsg:
|
||
Helper.OnLogMsg(f'WARNING: {msg}', (255, 128, 0))
|
||
caller = inspect.stack()[1]
|
||
logger.warning(f"[{Path(caller.filename).name}:{caller.lineno}.{caller.function}()] > {msg}")
|
||
if toWss:
|
||
Helper.log_wss({"type": "log", "kind": "WARNING", "msg" : msg} )
|
||
@staticmethod
|
||
def log_debug(msg, toWss = False):
|
||
if Helper.OnLogMsg:
|
||
Helper.OnLogMsg(f'DEBUG: {msg}', (0, 128, 128))
|
||
caller = inspect.stack()[1]
|
||
logger.debug(f"[{Path(caller.filename).name}:{caller.lineno}.{caller.function}()] > {msg}")
|
||
if toWss:
|
||
Helper.log_wss({"type": "log", "kind": "DEBUG", "msg" : msg} )
|
||
@staticmethod
|
||
def log_critical(msg, toWss = False):
|
||
if Helper.OnLogMsg:
|
||
Helper.OnLogMsg(f'CRITICAL: {msg}', (128, 0, 128))
|
||
caller = inspect.stack()[1]
|
||
logger.critical(f"[{Path(caller.filename).name}:{caller.lineno}.{caller.function}()] > {msg}")
|
||
if toWss:
|
||
Helper.log_wss({"type": "log", "kind": "CRITICAL", "msg" : msg} )
|
||
@staticmethod
|
||
def log_exception(msg, toWss = False):
|
||
if Helper.OnLogMsg:
|
||
Helper.OnLogMsg(f'EXCEPTION: {msg}', (255, 140, 0))
|
||
caller = inspect.stack()[1]
|
||
logger.exception(f"[{Path(caller.filename).name}:{caller.lineno}.{caller.function}()] > {msg}")
|
||
if toWss:
|
||
Helper.log_wss({"type": "log", "kind": "EXCEPTION", "msg" : msg} )
|
||
@staticmethod
|
||
def log(msg, toWss = False):
|
||
caller = inspect.stack()[1]
|
||
logger.log(f"[{Path(caller.filename).name}:{caller.lineno}.{caller.function}()] > {msg}")
|
||
if toWss:
|
||
Helper.log_wss({"type": "log", "kind": "LOG", "msg" : msg} )
|
||
@staticmethod
|
||
def log_wss(msg):
|
||
if Helper.wss:
|
||
Helper.wss.send(msg)
|
||
|
||
@staticmethod
|
||
def getTextSize(font, text):
|
||
import pygame as pg
|
||
surface = font.render(text, True, (0, 0, 0))
|
||
return (surface.get_width(), surface.get_height(), surface)
|
||
|
||
@staticmethod
|
||
def buildSurfaces(font, text, width, color, wordWrap):
|
||
text = text.strip()
|
||
w = Helper.getTextSize(font, text)[0]
|
||
result = []
|
||
if w > width and wordWrap:
|
||
segLen = math.floor(width / w * len(text))
|
||
while len(text):
|
||
if len(text) < segLen:
|
||
t = text
|
||
text = ''
|
||
else:
|
||
t = text[:segLen]
|
||
text = text[segLen:]
|
||
result.append(font.render(t, True, color))
|
||
else:
|
||
result.append(font.render(text, True, color))
|
||
return result
|
||
|
||
@staticmethod
|
||
def randomColor():
|
||
'''随机颜色'''
|
||
return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
|
||
|
||
@staticmethod
|
||
def reverseColor(color: QColor):
|
||
'''反转颜色'''
|
||
return (255 - color.red(), 255 - color.green(), 255 - color.blue())
|
||
@staticmethod
|
||
def getRGB(color_value):
|
||
# 如果是元组或列表形式的RGB值
|
||
if isinstance(color_value, (tuple, list)):
|
||
if len(color_value) >= 3:
|
||
# 取前三个值作为RGB
|
||
r, g, b = color_value[0], color_value[1], color_value[2]
|
||
# 确保值在0-255范围内
|
||
return (max(0, min(255, int(r))),
|
||
max(0, min(255, int(g))),
|
||
max(0, min(255, int(b))))
|
||
|
||
# 如果是整数形式的颜色值
|
||
elif isinstance(color_value, int):
|
||
# 将整数转换为RGB分量
|
||
# 假设格式为0xRRGGBB
|
||
r = (color_value >> 16) & 0xFF
|
||
g = (color_value >> 8) & 0xFF
|
||
b = color_value & 0xFF
|
||
return (r, g, b)
|
||
|
||
# 如果是字符串形式
|
||
elif isinstance(color_value, str):
|
||
# 处理十六进制颜色值
|
||
if color_value.startswith('#'):
|
||
hex_value = color_value[1:]
|
||
if len(hex_value) == 3: # 简写形式 #RGB
|
||
hex_value = ''.join([c*2 for c in hex_value])
|
||
if len(hex_value) in (6, 8): # #RRGGBB 或 #RRGGBBAA
|
||
r = int(hex_value[0:2], 16)
|
||
g = int(hex_value[2:4], 16)
|
||
b = int(hex_value[4:6], 16)
|
||
return (r, g, b)
|
||
# 处理颜色名称(需要额外的颜色名称映射表)
|
||
# 这里只列举几种常见颜色
|
||
color_names = {
|
||
'black': (0, 0, 0),
|
||
'white': (255, 255, 255),
|
||
'red': (255, 0, 0),
|
||
'green': (0, 255, 0),
|
||
'blue': (0, 0, 255),
|
||
'yellow': (255, 255, 0),
|
||
'magenta': (255, 0, 255),
|
||
'cyan': (0, 255, 255),
|
||
'orange': (255, 128, 0), # 根据项目规范
|
||
'teal': (0, 128, 128) # 根据项目规范
|
||
}
|
||
if color_value.lower() in color_names:
|
||
return color_names[color_value.lower()]
|
||
|
||
# 如果是Color对象(如pygame.Color)
|
||
elif hasattr(color_value, 'r') and hasattr(color_value, 'g') and hasattr(color_value, 'b'):
|
||
return (color_value.r, color_value.g, color_value.b)
|
||
|
||
# 默认返回黑色
|
||
return (0, 0, 0)
|
||
|
||
@staticmethod
|
||
def check_system_resources():
|
||
"""检查系统资源使用情况"""
|
||
logger.info("检查系统资源使用情况...")
|
||
cpu_percent = psutil.cpu_percent(interval=1)
|
||
memory = psutil.virtual_memory()
|
||
network = psutil.net_io_counters()
|
||
|
||
logger.info("检查系统资源使用情况完毕")
|
||
|
||
return {
|
||
'cpu_percent': cpu_percent,
|
||
'memory_percent': memory.percent,
|
||
'memory_available': memory.available / (1024**3), # GB
|
||
'network_bytes_sent': int(network.bytes_sent / 1024),
|
||
'network_bytes_recv': int(network.bytes_recv / 1024)
|
||
}
|
||
|
||
@staticmethod
|
||
def build_response(type, status : enums.Response, msg):
|
||
status_code, status_msg = status.value
|
||
result = {
|
||
"type": "response",
|
||
"request_type": type,
|
||
"status_code": status_code,
|
||
"status_msg": status_msg,
|
||
"detail_msg": msg
|
||
}
|
||
if status_code != 0:
|
||
Helper.error(result);
|
||
return result
|
||
|
||
@staticmethod
|
||
def get_surrounding_rect(points):
|
||
if len(points) == 0:
|
||
return Constant.invalid_rect
|
||
min_x = min(p.x() for p in points)
|
||
min_y = min(p.y() for p in points)
|
||
max_x = max(p.x() for p in points)
|
||
max_y = max(p.y() for p in points)
|
||
return QRectF(min_x, min_y, max_x - min_x, max_y - min_y)
|
||
|
||
@staticmethod
|
||
def getYoloLabellingInfo(dir_path, file_names, desc):
|
||
if len(dir_path) > 0:
|
||
imageNumber, labelNumber = 0, 0
|
||
imagePath = dir_path + 'images/'
|
||
labelPath = dir_path + 'labels/'
|
||
for file_name in file_names:
|
||
if file_name.startswith(imagePath):
|
||
imageNumber += 1
|
||
elif file_name.startswith(labelPath):
|
||
labelNumber += 1
|
||
return f'{desc} {imageNumber - 1} 张图片,{labelNumber - 1} 张标签;', imageNumber - 1
|
||
return f'无{desc};', 0
|
||
|
||
@staticmethod
|
||
def getMarkdownRenderText(mdContent):
|
||
# 使用Python库直接将Markdown转换为HTML,避免JavaScript依赖
|
||
try:
|
||
# 尝试导入markdown库
|
||
import markdown
|
||
html_content = markdown.markdown(mdContent)
|
||
# 添加基本样式使其美观
|
||
styled_html = f"""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
max-width: 900px; margin: 20px auto; padding: 0 20px; line-height: 1.6; }}
|
||
code {{ background: #f5f5f5; padding: 2px 4px; border-radius: 3px; }}
|
||
pre {{ background: #f5f5f5; padding: 10px; border-radius: 5px; overflow: auto; }}
|
||
pre code {{ background: none; padding: 0; }}
|
||
h1, h2, h3 {{ color: #333; border-bottom: 1px solid #eee; padding-bottom: 5px; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
{html_content}
|
||
</body>
|
||
</html>
|
||
"""
|
||
return styled_html
|
||
except ImportError:
|
||
# 回退到JavaScript的marked.js方法
|
||
logger.warning("未找到markdown库,使用JavaScript渲染方式")
|
||
# 从appIOs/configs加载marked.js
|
||
file_js = QFile('appIOs/configs/marked.min.js')
|
||
markedJs = ''
|
||
if file_js.open(QIODevice.ReadOnly | QIODevice.Text):
|
||
markedJs = file_js.readAll().data().decode('utf-8')
|
||
file_js.close()
|
||
|
||
# 转义markdown内容
|
||
escapedMd = mdContent.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')
|
||
|
||
# 创建HTML模板
|
||
htmlTemplate = '''
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
max-width: 900px; margin: 20px auto; padding: 0 20px; line-height: 1.6; }
|
||
code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; }
|
||
pre { background: #f5f5f5; padding: 10px; border-radius: 5px; overflow: auto; }
|
||
pre code { background: none; padding: 0; }
|
||
h1, h2, h3 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
||
</style>
|
||
<script>
|
||
// 加载marked库
|
||
%1
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<div id="content"></div>
|
||
<script>
|
||
const md = `%2`;
|
||
document.getElementById('content').innerHTML = marked.parse(md);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
# 生成HTML内容
|
||
htmlContent = htmlTemplate.replace('%1', markedJs).replace('%2', escapedMd)
|
||
return htmlContent
|
||
@staticmethod
|
||
def getMarkdownRender(mdFileName):
|
||
file = QFile(mdFileName)
|
||
mdContent = ''
|
||
if file.open(QIODevice.ReadOnly | QIODevice.Text):
|
||
stream = QTextStream(file)
|
||
stream.setAutoDetectUnicode(True)
|
||
mdContent = stream.readAll()
|
||
file.close()
|
||
return Helper.getMarkdownRenderText(mdContent)
|
||
else:
|
||
logger.error(f'打开文件 {mdFileName} 失败')
|
||
return f"<p style='color: red;'>无法打开文件: {mdFileName}</p>"
|
||
|
||
class RTTI:
|
||
@staticmethod
|
||
def _do_set_attr(obj, property_name, property_value):
|
||
if obj is None:
|
||
logger.error(f"RTTI.set: obj is None")
|
||
return
|
||
|
||
class_name = type(obj).__name__
|
||
object_name = obj.objectName()
|
||
if property_name not in dir(obj):
|
||
logger.error(f"RTTI.set: {class_name} {object_name}.{property_name} not in dir(obj)")
|
||
return
|
||
if property_name.endswith('icon') and isinstance(property_value, str):
|
||
original_property_value = property_value
|
||
if not os.path.exists(property_value):
|
||
property_value = os.path.join('appIOs/res/images/icons',property_value)
|
||
# logger.info(f"RTTI.set: {class_name} {object_name}.{property_name} = {property_value}(自动匹配)")
|
||
if not os.path.exists(property_value):
|
||
logger.error(f"{original_property_value}文件不存在 > RTTI.set: {class_name} {object_name}.{property_name} = '{original_property_value}'")
|
||
return
|
||
property_value = QIcon(property_value)
|
||
setter_method = getattr(obj, f'set{property_name[0].upper() + property_name[1:]}')
|
||
setter_method(property_value)
|
||
@staticmethod
|
||
def set(obj, property_name, property_value):
|
||
property_list = property_name.split('.')
|
||
if len(property_list) == 1:
|
||
RTTI._do_set_attr(obj, property_name, property_value)
|
||
else:
|
||
dest_obj = obj
|
||
for i in range(len(property_list) - 1):
|
||
if not dest_obj:
|
||
logger.error(f"RTTI.set: {property_list.join('.')} not found")
|
||
return
|
||
dest_obj = getattr(dest_obj, property_list[i])
|
||
RTTI._do_set_attr(dest_obj, property_list[-1], property_value)
|
||
|
||
@staticmethod
|
||
def _do_get_attr(obj, property_name):
|
||
if obj is None:
|
||
logger.error(f"RTTI.get: obj is None")
|
||
return None, None
|
||
if property_name not in dir(obj):
|
||
logger.error(f"RTTI.get: {type(obj).__name__} {obj.objectName()}.{property_name} not in dir(obj)")
|
||
return None, None
|
||
# 返回属性类型与属性值
|
||
type_name = type(getattr(obj, property_name)).__name__
|
||
value = getattr(obj, property_name)
|
||
return type_name, value
|
||
# 取得属性类型与属性值 type, value = RTTI.get(obj, property_name)
|
||
@staticmethod
|
||
def get(obj, property_name):
|
||
property_list = property_name.split('.')
|
||
if len(property_list) == 1:
|
||
return RTTI._do_get_attr(obj, property_name)
|
||
else:
|
||
dest_obj = obj
|
||
for i in range(len(property_list) - 1):
|
||
if not dest_obj:
|
||
logger.error(f"RTTI.get: {property_list.join('.')} not found")
|
||
return None
|
||
dest_obj = getattr(dest_obj, property_list[i])
|
||
return RTTI._do_get_attr(dest_obj, property_list[-1])
|
||
|
||
class DrawHelper:
|
||
@staticmethod
|
||
def draw_dashed_line(mat, pt1, pt2, color, thickness=1, dash_length=10):
|
||
dist = ((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) ** 0.5
|
||
dashes = int(dist / dash_length)
|
||
for i in range(dashes):
|
||
start = (int(pt1[0] + (pt2[0] - pt1[0]) * i / dashes), int(pt1[1] + (pt2[1] - pt1[1]) * i / dashes))
|
||
end = (int(pt1[0] + (pt2[0] - pt1[0]) * (i + 0.5) / dashes), int(pt1[1] + (pt2[1] - pt1[1]) * (i + 0.5) / dashes))
|
||
cv2.line(mat, start, end, color, thickness)
|
||
|
||
@staticmethod
|
||
def draw_dashed_rect(painter, rect, color, thickness=1, dash_length=10):
|
||
x1, y1 = rect.left(), rect.top()
|
||
x2, y2 = rect.right(), rect.bottom()
|
||
DrawHelper.draw_dashed_line(painter, (x1, y1), (x2, y1), color, thickness, dash_length)
|
||
DrawHelper.draw_dashed_line(painter, (x1, y2), (x2, y2), color, thickness, dash_length)
|
||
DrawHelper.draw_dashed_line(painter, (x1, y1), (x1, y2), color, thickness, dash_length)
|
||
DrawHelper.draw_dashed_line(painter, (x2, y1), (x2, y2), color, thickness, dash_length) |