
在工业制造和数控加工领域,刀具文件的管理和编辑是一项至关重要的工作。传统的文本编辑方式不仅效率低下,而且容易出错。今天,我们将深入探讨如何用Python+PyQt5构建一个功能完整的刀具文件编辑器,实现TLS格式文件的解析、编辑、可视化与保存。
本编辑器专为处理NR-VERSION 4.0.0t格式的TLS刀具文件设计,能够精确解析文件中的复杂数据结构,包括刀具的几何参数、装配信息、旋转速度等关键数据。通过图形化界面,用户可以直观地查看和编辑刀具规格,并实时预览刀具的三维轮廓。
整个系统采用分层架构,从底层的数据模型到上层的用户界面,每一层都有清晰的职责划分:
数据层:通过dataclass定义了完整的刀具数据结构,包括Point、Arc、SOR、Assembly和Tool等核心类,确保了类型安全和数据完整性。
解析层:TLSParser类负责读取TLS文件,通过正则表达式和自定义的括号匹配算法,精确提取文件中的结构化信息。该解析器能够处理嵌套的大括号结构,准确分离不同工具的数据块。
业务逻辑层:TLSCalculator(在代码中为_calculate_geometry方法)从解析出的SOR数据中自动计算几何参数,如直径、深度等,实现了从原始坐标到工程参数的转换。
视图层:ToolCanvas类提供刀具的可视化展示,通过坐标变换和缩放算法,将刀具的几何数据转化为直观的图形表示。
控制层:MainWindow和ToolEditDialog负责协调各组件的工作,处理用户交互,并调用TLSWriter将修改后的数据写回文件。
TLS文件采用嵌套的括号结构存储数据,这对解析算法提出了挑战。我们实现了extract_brace_content函数,通过栈式括号匹配算法,能够正确处理任意深度的嵌套结构:
defextract_brace_content(text, start_idx):
"""从指定位置开始提取匹配的大括号内容"""
if text[start_idx] != '{':
returnNone, start_idx
count = 0
end_idx = start_idx
for i in range(start_idx, len(text)):
if text[i] == '{':
count += 1
elif text[i] == '}':
count -= 1
if count == 0:
end_idx = i
break
return text[start_idx+1:end_idx], end_idx

从SOR(旋转体)的PTS点集和ARC圆弧数据中,我们可以推导出刀具的关键几何参数。直径计算基于X坐标的最大值,深度计算基于Y坐标的范围,圆角半径直接从ARC数据中提取:

ToolCanvas类实现了自适应的坐标变换系统,能够根据刀具的实际尺寸和画布大小,自动计算最佳缩放比例:
# 计算缩放比例
x_range = max_x - min_x
y_range = max_y - min_y
if x_range == 0or y_range == 0:
return
scale_x = available_width / x_range
scale_y = available_height / y_range
scale = min(scale_x, scale_y) * 0.8
编辑器实现了模型-视图的双向数据绑定。当用户在界面中修改参数时,相应的Tool对象会实时更新;反之,当加载新的刀具数据时,界面控件会自动同步显示最新值。
以下是完整的刀具文件编辑器代码,包含了所有必要的模块和功能:
import sys
import re
import os
from dataclasses import dataclass, field
from typing import List, Tuple, Optional, Dict
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QTabWidget, QCheckBox,
QFileDialog, QMessageBox, QGroupBox, QGridLayout,
QDialog, QDialogButtonBox, QComboBox, QSpinBox,
QDoubleSpinBox, QFrame, QScrollArea, QSplitter)
from PyQt5.QtCore import Qt, QRectF, QPointF
from PyQt5.QtGui import QPainter, QPen, QColor, QBrush, QFont, QPolygonF
@dataclass
classPoint:
x: float
y: float
@dataclass
classArc:
x: float
y: float
r: float
@dataclass
classSOR:
points: List[Point] = field(default_factory=list)
arcs: List[Arc] = field(default_factory=list)
@dataclass
classAssembly:
sors: List[SOR] = field(default_factory=list)
@dataclass
classTool:
tool_id: str = ""
units: str = "MILLIMETER"
description: str = ""
gagepoint_offset: Tuple[float, float, float] = (0, 0, 0)
tooltype: str = "MILLING"
cuttercolor: Tuple[int, int, int] = (255, 0, 0)
maxremovalrate: float = 0
max_qw: float = 0
stack: str = "NO"
cutter: Assembly = field(default_factory=Assembly)
holder: Assembly = field(default_factory=Assembly)
# 几何参数(从SOR计算或存储)
diameter: float = 0# 直径
corner_radius: float = 0# 圆角
outer_depth: float = 0# 外部深度
inner_depth: float = 0# 内部深度
wheel_width: float = 0# 砂轮磨料宽度
outer_angle: float = 0# 外角
inner_angle: float = 0# 内角
defextract_brace_content(text, start_idx):
"""从指定位置开始提取匹配的大括号内容"""
if text[start_idx] != '{':
returnNone, start_idx
count = 0
end_idx = start_idx
for i in range(start_idx, len(text)):
if text[i] == '{':
count += 1
elif text[i] == '}':
count -= 1
if count == 0:
end_idx = i
break
return text[start_idx+1:end_idx], end_idx
classTLSParser:
"""TLS文件解析器"""
def__init__(self, filepath: str):
self.filepath = filepath
self.tools: List[Tool] = []
self.raw_content = ""
defparse(self) -> List[Tool]:
"""解析TLS文件"""
with open(self.filepath, 'r', encoding='utf-8') as f:
self.raw_content = f.read()
# 查找所有TOOLID
tool_pattern = r'TOOLID\s*"(\d+)"\s*\{'
matches = list(re.finditer(tool_pattern, self.raw_content))
for i, match in enumerate(matches):
tool_id = match.group(1)
start_idx = match.end() - 1# 指向 { 的位置
content, end_idx = extract_brace_content(self.raw_content, start_idx)
if content:
tool = self._parse_tool(tool_id, content)
self.tools.append(tool)
return self.tools
def_parse_tool(self, tool_id: str, content: str) -> Tool:
"""解析单个工具"""
tool = Tool(tool_id=tool_id)
# 解析基本属性
units_match = re.search(r'UNITS\s+(\w+)', content)
if units_match:
tool.units = units_match.group(1)
desc_match = re.search(r'DESCRIPTION\s+"([^"]+)"', content)
if desc_match:
tool.description = desc_match.group(1)
gage_match = re.search(r'GAGEPOINT_OFFSET\s+\(([^)]+)\)', content)
if gage_match:
coords = [float(x.strip()) for x in gage_match.group(1).split(',')]
tool.gagepoint_offset = tuple(coords)
tooltype_match = re.search(r'TOOLTYPE\s+(\w+)', content)
if tooltype_match:
tool.tooltype = tooltype_match.group(1)
color_match = re.search(r'CUTTERCOLOR\s+\(([^)]+)\)', content)
if color_match:
rgb = [int(x.strip()) for x in color_match.group(1).split()]
tool.cuttercolor = tuple(rgb)
removal_match = re.search(r'MAXREMOVALRATE\s+\(([^)]+)\)', content)
if removal_match:
tool.maxremovalrate = float(removal_match.group(1))
qw_match = re.search(r'MAX-QW\s+\(([^)]+)\)', content)
if qw_match:
tool.max_qw = float(qw_match.group(1))
# 解析CUTTER - 使用大括号提取
cutter_match = re.search(r'CUTTER\s*\{', content)
if cutter_match:
start_idx = cutter_match.end() - 1
cutter_content, _ = extract_brace_content(content, start_idx)
if cutter_content:
tool.cutter = self._parse_assembly(cutter_content)
# 解析HOLDER
holder_match = re.search(r'HOLDER\s*\{', content)
if holder_match:
start_idx = holder_match.end() - 1
holder_content, _ = extract_brace_content(content, start_idx)
if holder_content:
tool.holder = self._parse_assembly(holder_content)
# 从SOR数据计算几何参数
self._calculate_geometry(tool)
return tool
def_parse_assembly(self, content: str) -> Assembly:
"""解析ASSEMBLY"""
assembly = Assembly()
# 查找所有SOR
sor_pattern = r'SOR\s*\{'
sor_matches = list(re.finditer(sor_pattern, content))
for sor_match in sor_matches:
start_idx = sor_match.end() - 1
sor_content, _ = extract_brace_content(content, start_idx)
if sor_content:
sor = SOR()
# 解析PTS
pts_pattern = r'PTS\s*\{'
pts_match = re.search(pts_pattern, sor_content)
if pts_match:
pts_start = pts_match.end() - 1
pts_str, _ = extract_brace_content(sor_content, pts_start)
if pts_str:
point_pattern = r'\(([^)]+)\)'
point_matches = re.findall(point_pattern, pts_str)
for pt in point_matches:
coords = [float(x.strip()) for x in pt.split(',')]
if len(coords) >= 2:
sor.points.append(Point(coords[0], coords[1]))
# 解析ARC
arc_pattern = r'ARC\s+\(([^)]+)\)'
arc_matches = re.findall(arc_pattern, sor_content)
for arc_str in arc_matches:
coords = [float(x.strip()) for x in arc_str.split(',')]
if len(coords) >= 3:
sor.arcs.append(Arc(coords[0], coords[1], coords[2]))
assembly.sors.append(sor)
return assembly
def_calculate_geometry(self, tool: Tool):
"""从SOR数据计算几何参数"""
# 从第一个SOR计算直径和深度
if tool.cutter.sors:
sor = tool.cutter.sors[0]
if sor.points:
# 计算最大直径(X坐标最大值*2)
max_x = max([p.x for p in sor.points])
tool.diameter = max_x * 2
# 计算深度(Y坐标范围)
y_coords = [p.y for p in sor.points]
if y_coords:
tool.outer_depth = max(y_coords)
tool.inner_depth = min(y_coords)
# 从圆弧计算圆角
if tool.cutter.sors:
for sor in tool.cutter.sors:
for arc in sor.arcs:
if arc.r > 0:
tool.corner_radius = arc.r
break
# 设置默认值
tool.wheel_width = 5.0
tool.outer_angle = 25.0
tool.inner_angle = 0.0
classToolCanvas(QWidget):
"""刀具示意图绘制控件"""
def__init__(self, parent=None):
super().__init__(parent)
self.tool = None
self.setMinimumSize(200, 400)
self.setSizePolicy(self.sizePolicy().Expanding, self.sizePolicy().Expanding)
defset_tool(self, tool: Tool):
self.tool = tool
self.update()
defpaintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 背景
painter.fillRect(self.rect(), QColor(240, 240, 240))
ifnot self.tool:
return
width = self.width()
height = self.height()
# 计算缩放比例
margin = 20
available_width = width - 2 * margin
available_height = height - 2 * margin
# 获取所有点的范围
all_points = []
for sor in self.tool.cutter.sors:
all_points.extend(sor.points)
for sor in self.tool.holder.sors:
all_points.extend(sor.points)
ifnot all_points:
return
min_x = min([p.x for p in all_points])
max_x = max([p.x for p in all_points])
min_y = min([p.y for p in all_points])
max_y = max([p.y for p in all_points])
x_range = max_x - min_x
y_range = max_y - min_y
if x_range == 0or y_range == 0:
return
scale_x = available_width / x_range
scale_y = available_height / y_range
scale = min(scale_x, scale_y) * 0.8
# 中心点
center_x = width / 2
center_y = height / 2
# 绘制HOLDER(刀柄)- 灰色
holder_color = QColor(180, 180, 180)
self._draw_assembly(painter, self.tool.holder, center_x, center_y, scale, holder_color)
# 绘制CUTTER(刀具)- 使用cuttercolor
cutter_color = QColor(*self.tool.cuttercolor)
self._draw_assembly(painter, self.tool.cutter, center_x, center_y, scale, cutter_color)
# 绘制中心线
pen = QPen(QColor(100, 100, 100), 1, Qt.DashLine)
painter.setPen(pen)
painter.drawLine(int(center_x), 0, int(center_x), height)
def_draw_assembly(self, painter: QPainter, assembly: Assembly,
center_x: float, center_y: float, scale: float, color: QColor):
"""绘制装配体"""
pen = QPen(QColor(50, 50, 50), 1)
painter.setPen(pen)
brush = QBrush(color)
painter.setBrush(brush)
for sor in assembly.sors:
if len(sor.points) < 2:
continue
# 创建多边形
polygon = QPolygonF()
# 添加左侧点(从上到下)
for pt in sor.points:
x = center_x - pt.x * scale
y = center_y - pt.y * scale
polygon.append(QPointF(x, y))
# 添加右侧点(从下到上)
for pt in reversed(sor.points):
x = center_x + pt.x * scale
y = center_y - pt.y * scale
polygon.append(QPointF(x, y))
painter.drawPolygon(polygon)
classGeometryTab(QWidget):
"""几何参数标签页"""
def__init__(self, parent=None):
super().__init__(parent)
self.tool = None
self.init_ui()
definit_ui(self):
layout = QHBoxLayout(self)
# 左侧参数区域
left_widget = QWidget()
left_layout = QGridLayout(left_widget)
left_layout.setColumnStretch(1, 1)
left_layout.setHorizontalSpacing(10)
left_layout.setVerticalSpacing(8)
# 直径
left_layout.addWidget(QLabel("直径:"), 0, 0)
self.diameter_edit = QLineEdit()
self.diameter_edit.setText("99.000")
left_layout.addWidget(self.diameter_edit, 0, 1)
left_layout.addWidget(QLabel("mm"), 0, 2)
# 圆角
left_layout.addWidget(QLabel("圆角:"), 1, 0)
self.corner_edit = QLineEdit()
self.corner_edit.setText("0.020")
left_layout.addWidget(self.corner_edit, 1, 1)
left_layout.addWidget(QLabel("mm"), 1, 2)
# 外部深度
left_layout.addWidget(QLabel("外部深度:"), 2, 0)
self.outer_depth_edit = QLineEdit()
self.outer_depth_edit.setText("25.000")
left_layout.addWidget(self.outer_depth_edit, 2, 1)
left_layout.addWidget(QLabel("mm"), 2, 2)
# 内部深度
left_layout.addWidget(QLabel("内部深度:"), 3, 0)
self.inner_depth_edit = QLineEdit()
self.inner_depth_edit.setText("16.667")
left_layout.addWidget(self.inner_depth_edit, 3, 1)
left_layout.addWidget(QLabel("mm"), 3, 2)
# 自复选框
self.auto_check = QCheckBox("自:")
left_layout.addWidget(self.auto_check, 3, 3)
# 砂轮磨料宽度
left_layout.addWidget(QLabel("砂轮磨料宽度:"), 4, 0)
self.wheel_width_edit = QLineEdit()
self.wheel_width_edit.setText("5.000")
left_layout.addWidget(self.wheel_width_edit, 4, 1)
left_layout.addWidget(QLabel("mm"), 4, 2)
# 外角
left_layout.addWidget(QLabel("外角:"), 5, 0)
self.outer_angle_edit = QLineEdit()
self.outer_angle_edit.setText("25.000")
left_layout.addWidget(self.outer_angle_edit, 5, 1)
left_layout.addWidget(QLabel("°"), 5, 2)
# 内角
left_layout.addWidget(QLabel("内角:"), 6, 0)
self.inner_angle_edit = QLineEdit()
self.inner_angle_edit.setText("0.000")
left_layout.addWidget(self.inner_angle_edit, 6, 1)
left_layout.addWidget(QLabel("°"), 6, 2)
# 添加弹簧空间
left_layout.setRowStretch(7, 1)
layout.addWidget(left_widget, 2)
# 右侧示意图区域
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
# 刀具示意图
self.canvas = ToolCanvas()
right_layout.addWidget(self.canvas, 1)
# 按钮
btn_layout = QHBoxLayout()
self.show_btn = QPushButton("显示...")
self.wheel_group_btn = QPushButton("砂轮组...")
btn_layout.addStretch()
btn_layout.addWidget(self.show_btn)
btn_layout.addWidget(self.wheel_group_btn)
right_layout.addLayout(btn_layout)
layout.addWidget(right_widget, 1)
defset_tool(self, tool: Tool):
"""设置工具数据"""
self.tool = tool
if tool:
self.diameter_edit.setText(f"{tool.diameter:.3f}")
self.corner_edit.setText(f"{tool.corner_radius:.3f}")
self.outer_depth_edit.setText(f"{tool.outer_depth:.3f}")
self.inner_depth_edit.setText(f"{tool.inner_depth:.3f}")
self.wheel_width_edit.setText(f"{tool.wheel_width:.3f}")
self.outer_angle_edit.setText(f"{tool.outer_angle:.3f}")
self.inner_angle_edit.setText(f"{tool.inner_angle:.3f}")
self.canvas.set_tool(tool)
defget_geometry_data(self) -> Dict:
"""获取几何参数数据"""
return {
'diameter': float(self.diameter_edit.text() or0),
'corner_radius': float(self.corner_edit.text() or0),
'outer_depth': float(self.outer_depth_edit.text() or0),
'inner_depth': float(self.inner_depth_edit.text() or0),
'wheel_width': float(self.wheel_width_edit.text() or0),
'outer_angle': float(self.outer_angle_edit.text() or0),
'inner_angle': float(self.inner_angle_edit.text() or0),
}
classInfoTab(QWidget):
"""信息标签页"""
def__init__(self, parent=None):
super().__init__(parent)
self.init_ui()
definit_ui(self):
layout = QGridLayout(self)
# 工具ID
layout.addWidget(QLabel("工具ID:"), 0, 0)
self.tool_id_edit = QLineEdit()
self.tool_id_edit.setReadOnly(True)
layout.addWidget(self.tool_id_edit, 0, 1)
# 描述
layout.addWidget(QLabel("描述:"), 1, 0)
self.desc_edit = QLineEdit()
layout.addWidget(self.desc_edit, 1, 1)
# 单位
layout.addWidget(QLabel("单位:"), 2, 0)
self.units_combo = QComboBox()
self.units_combo.addItems(["MILLIMETER", "INCH"])
layout.addWidget(self.units_combo, 2, 1)
# 工具类型
layout.addWidget(QLabel("工具类型:"), 3, 0)
self.type_edit = QLineEdit()
layout.addWidget(self.type_edit, 3, 1)
# 测量点偏移
layout.addWidget(QLabel("测量点偏移:"), 4, 0)
self.gagepoint_edit = QLineEdit()
layout.addWidget(self.gagepoint_edit, 4, 1)
# 颜色
layout.addWidget(QLabel("颜色 (R G B):"), 5, 0)
self.color_edit = QLineEdit()
layout.addWidget(self.color_edit, 5, 1)
# 最大移除率
layout.addWidget(QLabel("最大移除率:"), 6, 0)
self.max_removal_edit = QLineEdit()
layout.addWidget(self.max_removal_edit, 6, 1)
# MAX-QW
layout.addWidget(QLabel("MAX-QW:"), 7, 0)
self.max_qw_edit = QLineEdit()
layout.addWidget(self.max_qw_edit, 7, 1)
layout.setRowStretch(8, 1)
defset_tool(self, tool: Tool):
"""设置工具数据"""
if tool:
self.tool_id_edit.setText(tool.tool_id)
self.desc_edit.setText(tool.description)
self.units_combo.setCurrentText(tool.units)
self.type_edit.setText(tool.tooltype)
self.gagepoint_edit.setText(f"{tool.gagepoint_offset}")
self.color_edit.setText(f"{tool.cuttercolor[0]}{tool.cuttercolor[1]}{tool.cuttercolor[2]}")
self.max_removal_edit.setText(str(tool.maxremovalrate))
self.max_qw_edit.setText(str(tool.max_qw))
defget_info_data(self) -> Dict:
"""获取信息数据"""
color_str = self.color_edit.text().split()
color = (int(color_str[0]), int(color_str[1]), int(color_str[2])) if len(color_str) >= 3else (255, 0, 0)
return {
'description': self.desc_edit.text(),
'units': self.units_combo.currentText(),
'tooltype': self.type_edit.text(),
'cuttercolor': color,
'maxremovalrate': float(self.max_removal_edit.text() or0),
'max_qw': float(self.max_qw_edit.text() or0),
}
classRotationTab(QWidget):
"""旋转速度标签页"""
def__init__(self, parent=None):
super().__init__(parent)
self.init_ui()
definit_ui(self):
layout = QGridLayout(self)
layout.addWidget(QLabel("主轴转速:"), 0, 0)
self.spindle_speed = QLineEdit()
self.spindle_speed.setText("10000")
layout.addWidget(self.spindle_speed, 0, 1)
layout.addWidget(QLabel("RPM"), 0, 2)
layout.addWidget(QLabel("进给速度:"), 1, 0)
self.feed_rate = QLineEdit()
self.feed_rate.setText("1000")
layout.addWidget(self.feed_rate, 1, 1)
layout.addWidget(QLabel("mm/min"), 1, 2)
layout.setRowStretch(2, 1)
classToolEditDialog(QDialog):
"""工具编辑对话框"""
def__init__(self, tool: Tool, parent=None):
super().__init__(parent)
self.tool = tool
self.original_tool = tool
self.setWindowTitle(f"柱体/后角 1 - 砂轮规格 ({tool.description})")
self.resize(700, 500)
self.init_ui()
definit_ui(self):
layout = QVBoxLayout(self)
# 标签页
self.tabs = QTabWidget()
# 信息标签页
self.info_tab = InfoTab()
self.info_tab.set_tool(self.tool)
self.tabs.addTab(self.info_tab, "信息")
# 几何参数标签页
self.geometry_tab = GeometryTab()
self.geometry_tab.set_tool(self.tool)
self.tabs.addTab(self.geometry_tab, "几何参数")
# 旋转速度标签页
self.rotation_tab = RotationTab()
self.tabs.addTab(self.rotation_tab, "旋转速度")
layout.addWidget(self.tabs)
# 底部按钮区域
bottom_layout = QHBoxLayout()
# 左侧快捷键提示
shortcut_label = QLabel("F2 F3 F10")
bottom_layout.addWidget(shortcut_label)
bottom_layout.addStretch()
# 选择按钮
self.select_btn = QPushButton("选择...")
self.select_btn.clicked.connect(self.on_select)
bottom_layout.addWidget(self.select_btn)
bottom_layout.addStretch()
# OK和取消按钮
self.ok_btn = QPushButton("OK")
self.ok_btn.clicked.connect(self.on_ok)
bottom_layout.addWidget(self.ok_btn)
self.cancel_btn = QPushButton("取消")
self.cancel_btn.clicked.connect(self.reject)
bottom_layout.addWidget(self.cancel_btn)
# 帮助按钮
self.help_btn = QPushButton("?")
self.help_btn.setFixedWidth(30)
bottom_layout.addWidget(self.help_btn)
layout.addLayout(bottom_layout)
defon_select(self):
"""选择其他工具"""
pass
defon_ok(self):
"""确认按钮"""
# 更新工具数据
info_data = self.info_tab.get_info_data()
geo_data = self.geometry_tab.get_geometry_data()
self.tool.description = info_data['description']
self.tool.units = info_data['units']
self.tool.tooltype = info_data['tooltype']
self.tool.cuttercolor = info_data['cuttercolor']
self.tool.maxremovalrate = info_data['maxremovalrate']
self.tool.max_qw = info_data['max_qw']
self.tool.diameter = geo_data['diameter']
self.tool.corner_radius = geo_data['corner_radius']
self.tool.outer_depth = geo_data['outer_depth']
self.tool.inner_depth = geo_data['inner_depth']
self.tool.wheel_width = geo_data['wheel_width']
self.tool.outer_angle = geo_data['outer_angle']
self.tool.inner_angle = geo_data['inner_angle']
self.accept()
classTLSWriter:
"""TLS文件写入器"""
def__init__(self, filepath: str):
self.filepath = filepath
defwrite(self, tools: List[Tool], original_content: str = ""):
"""写入TLS文件"""
lines = []
# 写入版本头
if original_content:
version_match = re.search(r'\(NR-VERSION[^)]*\)', original_content)
if version_match:
lines.append(version_match.group(0))
else:
lines.append("(NR-VERSION 4.0.0t)")
else:
lines.append("(NR-VERSION 4.0.0t)")
lines.append("")
lines.append("")
for tool in tools:
lines.append(f'TOOLID "{tool.tool_id}" {{')
lines.append("")
lines.append(f'\tUNITS {tool.units}')
lines.append("")
lines.append(f'\tDESCRIPTION "{tool.description}"')
lines.append("")
lines.append(f'\tGAGEPOINT_OFFSET {tool.gagepoint_offset}')
lines.append("")
lines.append(f'\tTOOLTYPE {tool.tooltype}')
lines.append("")
lines.append(f'\tCUTTERCOLOR ({tool.cuttercolor[0]}{tool.cuttercolor[1]}{tool.cuttercolor[2]})')
lines.append("")
lines.append(f'\tMAXREMOVALRATE ({tool.maxremovalrate})')
lines.append("")
lines.append(f'\tMAX-QW ({tool.max_qw})')
lines.append("")
lines.append(f'\tSTACK {tool.stack}')
lines.append("")
# CUTTER
lines.append('\tCUTTER {')
lines.append('')
lines.append('\t\tASSEMBLY {')
lines.append('')
for sor in tool.cutter.sors:
lines.append('\t\t\tSOR {')
# PTS
if sor.points:
pts_str = " ".join([f"({p.x},{p.y})"for p in sor.points])
lines.append(f'\t\t\t\tPTS {{ {pts_str} }}')
# ARC
for arc in sor.arcs:
lines.append(f'\t\t\t\tARC ({arc.x},{arc.y},{arc.r})')
lines.append('\t\t\t}')
lines.append('\t\t}')
lines.append('')
lines.append('\t}')
lines.append("")
# HOLDER
lines.append('\tHOLDER {')
lines.append('')
lines.append('\t\tASSEMBLY {')
lines.append('')
for sor in tool.holder.sors:
lines.append('\t\t\tSOR {')
# PTS
if sor.points:
pts_str = " ".join([f"({p.x},{p.y})"for p in sor.points])
lines.append(f'\t\t\t\tPTS {{ {pts_str} }}')
# ARC
for arc in sor.arcs:
lines.append(f'\t\t\t\tARC ({arc.x},{arc.y},{arc.r})')
lines.append('\t\t\t}')
lines.append('\t\t}')
lines.append('')
lines.append('\t}')
lines.append("")
lines.append('}')
lines.append("")
lines.append("")
# 写入文件
with open(self.filepath, 'w', encoding='utf-8') as f:
f.write("\n".join(lines))
classMainWindow(QMainWindow):
"""主窗口"""
def__init__(self):
super().__init__()
self.setWindowTitle("刀具文件编辑器")
self.resize(900, 600)
self.filepath = r"D:\num4.0T\Temp\numroto3d\test_ball.tls"
self.tools: List[Tool] = []
self.current_tool: Optional[Tool] = None
self.original_content = ""
self.init_ui()
self.load_default_file()
definit_ui(self):
# 中央部件
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# 文件选择区域
file_layout = QHBoxLayout()
file_layout.addWidget(QLabel("文件路径:"))
self.file_path_edit = QLineEdit()
self.file_path_edit.setText(self.filepath)
file_layout.addWidget(self.file_path_edit, 1)
self.browse_btn = QPushButton("浏览...")
self.browse_btn.clicked.connect(self.on_browse)
file_layout.addWidget(self.browse_btn)
self.load_btn = QPushButton("加载")
self.load_btn.clicked.connect(self.on_load)
file_layout.addWidget(self.load_btn)
layout.addLayout(file_layout)
# 分隔线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
layout.addWidget(line)
# 工具列表区域
list_layout = QHBoxLayout()
# 工具列表
list_layout.addWidget(QLabel("工具列表:"))
self.tool_combo = QComboBox()
self.tool_combo.currentIndexChanged.connect(self.on_tool_selected)
list_layout.addWidget(self.tool_combo, 1)
self.edit_btn = QPushButton("编辑选中工具")
self.edit_btn.clicked.connect(self.on_edit_tool)
list_layout.addWidget(self.edit_btn)
layout.addLayout(list_layout)
# 工具信息显示区域
self.info_text = QLabel("请加载文件并选择工具进行编辑")
self.info_text.setAlignment(Qt.AlignCenter)
self.info_text.setStyleSheet("QLabel { background-color: #f0f0f0; padding: 20px; }")
layout.addWidget(self.info_text, 1)
# 保存按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
self.save_btn = QPushButton("保存到文件")
self.save_btn.clicked.connect(self.on_save)
self.save_btn.setEnabled(False)
btn_layout.addWidget(self.save_btn)
layout.addLayout(btn_layout)
defload_default_file(self):
"""加载默认文件"""
if os.path.exists(self.filepath):
self.on_load()
defon_browse(self):
"""浏览文件"""
filepath, _ = QFileDialog.getOpenFileName(
self, "选择TLS文件", self.filepath, "TLS Files (*.tls);;All Files (*)")
if filepath:
self.filepath = filepath
self.file_path_edit.setText(filepath)
defon_load(self):
"""加载文件"""
self.filepath = self.file_path_edit.text()
ifnot os.path.exists(self.filepath):
QMessageBox.warning(self, "错误", f"文件不存在: {self.filepath}")
return
try:
# 读取原始内容
with open(self.filepath, 'r', encoding='utf-8') as f:
self.original_content = f.read()
# 解析文件
parser = TLSParser(self.filepath)
self.tools = parser.parse()
# 更新工具列表
self.tool_combo.clear()
for tool in self.tools:
self.tool_combo.addItem(f"工具 {tool.tool_id}: {tool.description}", tool)
if self.tools:
self.save_btn.setEnabled(True)
self.update_info_display()
QMessageBox.information(self, "成功", f"已加载 {len(self.tools)} 个工具")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载文件失败: {str(e)}")
defon_tool_selected(self, index):
"""选择工具"""
if index >= 0and index < len(self.tools):
self.current_tool = self.tools[index]
self.update_info_display()
defupdate_info_display(self):
"""更新信息显示"""
if self.current_tool:
info = f"""
<h3>工具 {self.current_tool.tool_id}: {self.current_tool.description}</h3>
<table>
<tr><td><b>单位:</b></td><td>{self.current_tool.units}</td></tr>
<tr><td><b>类型:</b></td><td>{self.current_tool.tooltype}</td></tr>
<tr><td><b>测量点偏移:</b></td><td>{self.current_tool.gagepoint_offset}</td></tr>
<tr><td><b>颜色:</b></td><td>RGB{self.current_tool.cuttercolor}</td></tr>
<tr><td><b>最大移除率:</b></td><td>{self.current_tool.maxremovalrate}</td></tr>
<tr><td><b>MAX-QW:</b></td><td>{self.current_tool.max_qw}</td></tr>
<tr><td><b>直径:</b></td><td>{self.current_tool.diameter:.3f} mm</td></tr>
<tr><td><b>圆角:</b></td><td>{self.current_tool.corner_radius:.3f} mm</td></tr>
<tr><td><b>外部深度:</b></td><td>{self.current_tool.outer_depth:.3f} mm</td></tr>
<tr><td><b>内部深度:</b></td><td>{self.current_tool.inner_depth:.3f} mm</td></tr>
</table>
"""
self.info_text.setText(info)
defon_edit_tool(self):
"""编辑工具"""
index = self.tool_combo.currentIndex()
if index < 0or index >= len(self.tools):
QMessageBox.warning(self, "警告", "请先选择一个工具")
return
tool = self.tools[index]
dialog = ToolEditDialog(tool, self)
if dialog.exec_() == QDialog.Accepted:
# 更新工具数据
self.tools[index] = dialog.tool
self.current_tool = dialog.tool
self.update_info_display()
QMessageBox.information(self, "成功", "工具参数已更新,请保存文件")
defon_save(self):
"""保存文件"""
ifnot self.tools:
QMessageBox.warning(self, "警告", "没有可保存的工具数据")
return
try:
writer = TLSWriter(self.filepath)
writer.write(self.tools, self.original_content)
QMessageBox.information(self, "成功", f"文件已保存到: {self.filepath}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
defmain():
app = QApplication(sys.argv)
# 设置应用程序样式
app.setStyle('Fusion')
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
这个刀具文件编辑器不仅适用于单个文件的编辑,还可以扩展为批量处理工具。在智能制造和数字孪生系统中,它可以作为刀具管理模块的核心组件,与CAD/CAM系统、机床控制系统和刀具库管理系统集成。
通过进一步开发,可以添加以下高级功能:
本文详细介绍了一个基于Python+PyQt5的刀具文件编辑器的完整实现,从数据结构设计、文件解析算法、图形界面构建到数据持久化存储,提供了一个完整的工业软件开发范例。这个项目展示了如何将复杂的工程问题分解为可管理的技术组件,并通过现代编程技术构建出专业级的应用程序。
该编辑器的价值不仅在于其功能本身,更在于它提供了一种处理复杂工程数据的范式:通过强类型的数据模型确保数据完整性,通过分层架构实现关注点分离,通过可视化技术提升用户体验。这种设计思路可以推广到其他工程软件和工业物联网应用中。


陪伴是最长情的告白
为你推送最实用的资讯

识别二维码 关注我们