<think>这个错误提示表明在设置状态栏文本时,使用的索引超出了状态栏字段的数量。在代码中,我们创建了一个有5个字段的状态栏,但是在更新状态栏时,我们可能尝试设置超过5个字段的文本(索引必须从0到4)。
查看代码中创建状态栏的部分:
self.status_bar = self.CreateStatusBar(5) # 5个状态栏字段
self.status_bar.SetStatusWidths([-1, 100, 100, 100, 100])
然后,在update_status方法中,我们为每个Tab生成一个状态文本,并且还有一个额外的“Ready”状态(放在索引0)。由于有5个Tab,所以我们生成了6个状态文本(索引0到5),但是状态栏只有5个字段(索引0到4)。因此,当循环设置状态文本时,当i=5会触发错误,因为状态栏字段索引最大为4。
解决方案:我们需要确保状态文本的个数不超过状态栏的字段数。在我们的设计中,状态栏字段数等于Tab数量(5个)加1(第一个字段用于显示通用状态)?实际上,我们创建了5个字段,第一个字段用于显示“Ready”,后面4个字段分别对应5个Tab?这显然不对,因为5个字段对应5个Tab,但第一个字段已经被“Ready”占用,那么后面的4个字段无法对应5个Tab。
实际上,我们设计的状态栏字段数量是5个,而我们有5个Tab,所以我们可以这样分配:
第0个字段:固定显示“Ready”等信息
第1-4个字段:分别对应5个Tab中的4个?不对,5个Tab需要5个字段。
因此,我们应该将第一个字段也用于一个Tab,这样5个字段对应5个Tab。但是这样就没有地方显示“Ready”了。或者我们可以改变设计:只显示5个Tab的统计信息,不显示“Ready”。或者我们使用6个字段,第一个字段用于通用信息,后面5个用于Tab。
但是,在创建状态栏时,我们指定了5个字段,并且设置了每个字段的宽度:
self.status_bar.SetStatusWidths([-1, 100, 100, 100, 100])
这意味着我们希望第一个字段宽度自适应,后面4个字段宽度100。但我们需要5个Tab的统计信息,因此我们需要6个字段?不对,因为我们的Tab只有5个,所以我们需要5个字段显示统计信息,再加上一个字段显示状态,总共6个字段。
因此,修改如下:
1. 创建状态栏时,指定6个字段:
self.status_bar = self.CreateStatusBar(6)
self.status_bar.SetStatusWidths([-1, 100, 100, 100, 100, 100]) # 6个字段
2. 在update_status方法中,我们生成6个状态文本(第一个是“Ready”,后面5个是每个Tab的统计),然后循环设置6个字段。
但是,注意我们之前代码中:
status_texts = ["Ready"]
for i, port in enumerate(self.tabs.keys()):
... # 添加5个统计文本
这样status_texts就有6个元素:索引0到5。
然后循环设置:
for i, text in enumerate(status_texts):
self.status_bar.SetStatusText(text, i)
这样i从0到5,而状态栏字段索引0到5,共6个,就不会越界。
但是,我们代码中原来只有5个字段,所以需要修改创建状态栏的代码。另外,在设置状态栏宽度时也需要6个值。
然而,我们也可以改变思路:不显示“Ready”,而是把5个字段全部用于Tab统计。这样就不需要第一个字段了。那么我们可以:
status_texts = []
for port in self.tabs.keys():
... # 为每个Tab生成统计文本,共5个
for i, text in enumerate(status_texts):
self.status_bar.SetStatusText(text, i)
然后,创建状态栏时还是5个字段,宽度设置也保持5个值。
根据需求,状态栏显示每个Tab的收发统计,所以我们可以不用“Ready”状态。因此,我选择第二种方式:去掉“Ready”,只显示5个Tab的统计。
修改update_status方法如下:
status_texts = []
for port in self.tabs.keys():
if port == 8005:
stat = f"Tx:{self.tx_count} Rx:{self.rx_stats.get(8005,0)}"
else:
stat = f"Rx:{self.rx_stats.get(port,0)}"
status_texts.append(stat)
for i, text in enumerate(status_texts):
if i < self.status_bar.GetFieldsCount(): # 确保索引不越界
self.status_bar.SetStatusText(text, i)
同时,在创建状态栏的时候,我们创建5个字段:
self.status_bar = self.CreateStatusBar(5)
self.status_bar.SetStatusWidths([-1, 100, 100, 100, 100]) # 5个字段,第一个自适应,后面4个100宽度
但是注意,我们可能有5个字段,但是我们的status_texts也是5个元素,所以循环i从0到4,不会越界。
另外,为了使布局合理,我们将第一个字段设置为自适应宽度(-1),后面的字段宽度100。这样第一个Tab的统计信息会占据更多空间(因为可能有更长的文本),而后面四个固定宽度。
但是,我们也可以根据实际需要调整宽度,比如所有字段都自适应,或者固定宽度。
修改后的完整update_status方法:
def update_status(self, event):
status_texts = []
# 按照Tab的顺序(端口号顺序)生成统计文本
ports = list(self.tabs.keys()) # 顺序为[8001,8002,8003,8004,8005]
for port in ports:
if port == 8005:
stat = f"Port{port}: Tx:{self.tx_count} Rx:{self.rx_stats.get(port,0)}"
else:
stat = f"Port{port}: Rx:{self.rx_stats.get(port,0)}"
status_texts.append(stat)
# 设置状态栏文本
for i in range(self.status_bar.GetFieldsCount()):
if i < len(status_texts):
self.status_bar.SetStatusText(status_texts[i], i)
else:
self.status_bar.SetStatusText("", i)
这样修改后,就不会出现越界错误。
但是,我们还需要注意:状态栏的字段数量必须和status_texts的数量一致(即5个)。由于我们在创建状态栏时指定了5个字段,而status_texts也是5个元素,所以没有问题。
然而,为了代码健壮性,我们在设置状态栏文本时,使用循环字段数,并用条件判断防止访问status_texts越界。
下面我们修改代码,并重新运行测试。
另外,状态栏的字段宽度设置:第一个字段我们设置为-1(自适应),后面四个100。如果第一个字段的文本太长,可能会挤压后面的空间。我们可以考虑将每个字段都设置为固定宽度,或者根据内容调整。这里为了简单,我们保持原样。
修改后的代码部分:
# 状态栏
self.status_bar = self.CreateStatusBar(5) # 5个状态栏字段
self.status_bar.SetStatusWidths([-1, 100, 100, 100, 100])
注意:SetStatusWidths的参数是一个包含5个整数的列表。
如果仍然出现字段索引错误,请检查status_texts的长度是否大于字段数(5),或者循环索引是否超过4。
由于我们修改了update_status方法,去掉了第一个“Ready”状态,并且只生成5个状态文本,所以应该不会再出现越界。
如果问题仍然存在,可以在循环中加入判断:
for i in range(self.status_bar.GetFieldsCount()):
if i < len(status_texts):
self.status_bar.SetStatusText(status_texts[i], i)
else:
self.status_bar.SetStatusText("", i)
这样即使status_texts少于字段数,多余的字段会被清空。
另外,在初始化状态栏时,我们也可以设置每个字段的初始文本:
for i in range(5):
self.status_bar.SetStatusText("", i)
这样初始状态为空。
修改后的完整update_status方法:
def update_status(self, event):
ports = list(self.tabs.keys()) # 顺序为[8001,8002,8003,8004,8005]
status_texts = []
for port in ports:
if port == 8005:
text = f"Port{port}: Tx:{self.tx_count} Rx:{self.rx_stats.get(port,0)}"
else:
text = f"Port{port}: Rx:{self.rx_stats.get(port,0)}"
status_texts.append(text)
# 确保状态栏每个字段都有文本(不超过字段数量)
for i in range(self.status_bar.GetFieldsCount()):
if i < len(status_texts):
self.status_bar.SetStatusText(status_texts[i], i)
else:
# 如果状态字段多于Tab,则清空多余字段
self.status_bar.SetStatusText("", i)
现在,我们重新运行程序,应该不会出现断言错误。
如果问题依旧,请仔细检查tab的数量和状态栏字段数量的一致性。我们创建了5个Tab(端口8001-8005),状态栏字段也是5个。
注意:在创建状态栏字段时,我们用了5个字段,而status_texts也是5个元素,所以循环i从0到4,设置状态栏字段0到4,不会越界。
因此,最终的修改方案如上。
由于我们无法在此环境中运行,请根据实际情况调整。如果还有其他错误,请提供详细信息。
为了完整起见,下面给出修改后的UDPMonitorFrame类的__init__方法中关于状态栏的部分,以及update_status方法:
在__init__方法中:
# 状态栏
self.status_bar = self.CreateStatusBar(5) # 5个状态栏字段
self.status_bar.SetStatusWidths([-1, 100, 100, 100, 100])
以及update_status方法:
def update_status(self, event):
"""更新状态栏统计信息"""
ports = list(self.tabs.keys()) # 获取5个端口号,顺序为初始化时的顺序[8001,8002,8003,8004,8005]
status_texts = []
for port in ports:
if port == 8005:
# 对于8005端口,同时显示发送和接收统计
stat_text = f"Port{port}: Tx:{self.tx_count} Rx:{self.rx_stats.get(port,0)}"
else:
stat_text = f"Port{port}: Rx:{self.rx_stats.get(port,0)}"
status_texts.append(stat_text)
# 设置状态栏文本,确保不超过字段数
for i in range(self.status_bar.GetFieldsCount()):
if i < len(status_texts):
self.status_bar.SetStatusText(status_texts[i], i)
else:
self.status_bar.SetStatusText("", i)
这样修改后,应该可以避免上述错误。
</think>这个错误是由于状态栏字段索引超出范围导致的。问题在于状态栏字段数量与代码中尝试设置的字段索引不匹配。让我修复这个问题并提供完整的解决方案:
```python
import wx
import socket
import threading
import time
import struct
import binascii
from collections import deque, defaultdict
class ProtocolDecoder:
BIT_LEVEL = 0
TV_FORMAT = 1
TLV_FORMAT = 2
def decode(self, data, format_type):
try:
if format_type == self.BIT_LEVEL:
return self._bit_level_decode(data)
elif format_type == self.TV_FORMAT:
return self._tv_decode(data)
elif format_type == self.TLV_FORMAT:
return self._tlv_decode(data)
except:
return {"Error": "Decoding failed"}
return {}
def _bit_level_decode(self, data):
decoded = {}
if len(data) >= 1:
byte_val = data[0] if isinstance(data[0], int) else ord(data[0])
decoded['Version'] = (byte_val & 0xF0) >> 4
decoded['Type'] = byte_val & 0x0F
return decoded
def _tv_decode(self, data):
decoded = {}
if len(data) >= 5:
t = data[0] if isinstance(data[0], int) else ord(data[0])
v = int.from_bytes(data[1:5], byteorder='big')
decoded[f'Type-{t}'] = v
return decoded
def _tlv_decode(self, data):
decoded = {}
pos = 0
while pos < len(data):
if pos + 2 > len(data):
break
tag = data[pos] if isinstance(data[pos], int) else ord(data[pos])
length = data[pos+1] if isinstance(data[pos+1], int) else ord(data[pos+1])
pos += 2
if pos + length > len(data):
break
value = data[pos:pos+length]
decoded[f'Tag-{tag}'] = value.hex()
pos += length
return decoded
class UDPMonitorTab(wx.Panel):
def __init__(self, parent, port, max_lines=200):
super().__init__(parent)
self.port = port
self.max_lines = max_lines
self.message_queue = deque(maxlen=max_lines)
self.paused = False
self.rx_count = 0
sizer = wx.BoxSizer(wx.HORIZONTAL)
self.msg_text = wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL|wx.TE_RICH2)
sizer.Add(self.msg_text, 1, wx.EXPAND|wx.ALL, 5)
self.tree = wx.TreeCtrl(self, style=wx.TR_DEFAULT_STYLE|wx.TR_HIDE_ROOT)
sizer.Add(self.tree, 1, wx.EXPAND|wx.ALL, 5)
self.SetSizer(sizer)
self.decoder = ProtocolDecoder()
self.root = self.tree.AddRoot("Root")
self.msg_text.Bind(wx.EVT_LEFT_DOWN, self.on_pause)
self.msg_text.Bind(wx.EVT_SET_FOCUS, self.on_focus)
def on_pause(self, event):
self.paused = not self.paused
event.Skip()
def on_focus(self, event):
"""当文本框获得焦点时自动滚动到底部"""
if not self.paused:
self.msg_text.SetInsertionPointEnd()
event.Skip()
def add_message(self, message, direction="RX"):
timestamp = time.strftime('%H:%M:%S')
colored_msg = f"[{timestamp}] [{direction}] {message}"
self.message_queue.append((direction, colored_msg))
if not self.paused:
self.refresh_display()
self.rx_count += 1
def refresh_display(self):
self.msg_text.Clear()
default_style = wx.TextAttr()
for direction, msg in self.message_queue:
self.msg_text.SetDefaultStyle(default_style)
if direction == "TX":
self.msg_text.SetDefaultStyle(wx.TextAttr(wx.Colour(0, 0, 255))) # 蓝色表示发送
elif direction == "RX":
self.msg_text.SetDefaultStyle(wx.TextAttr(wx.Colour(0, 128, 0))) # 绿色表示接收
self.msg_text.AppendText(msg + '\n')
# 滚动到底部
if not self.paused:
self.msg_text.SetInsertionPointEnd()
def parse_message(self, data):
self.tree.DeleteChildren(self.root)
# 尝试所有解码格式
formats = [self.decoder.BIT_LEVEL, self.decoder.TV_FORMAT, self.decoder.TLV_FORMAT]
found_valid = False
for fmt in formats:
decoded = self.decoder.decode(data, fmt)
if decoded:
found_valid = True
format_name = ["Bit-Level", "TV Format", "TLV Format"][fmt]
fmt_node = self.tree.AppendItem(self.root, f"Format: {format_name}")
for key, value in decoded.items():
child = self.tree.AppendItem(fmt_node, f"{key}: {value}")
break
if not found_valid:
error_node = self.tree.AppendItem(self.root, "Unable to decode message")
hex_node = self.tree.AppendItem(error_node, f"Hex: {data.hex()}")
len_node = self.tree.AppendItem(error_node, f"Length: {len(data)} bytes")
self.tree.ExpandAll()
class UDPMonitorFrame(wx.Frame):
def __init__(self):
super().__init__(None, title="UDP Monitor", size=(1200, 800))
self.tx_count = 0
self.rx_stats = defaultdict(int)
# 创建发送socket
self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.send_sock.bind(('0.0.0.0', 8000))
# 创建接收socket
self.recv_socks = {}
self.recv_threads = {}
self.ports = [8001, 8002, 8003, 8004, 8005]
# 创建UI
self.notebook = wx.Notebook(self)
self.tabs = {}
for port in self.ports:
tab = UDPMonitorTab(self.notebook, port)
self.tabs[port] = tab
self.notebook.AddPage(tab, f"Port {port}")
# 控制面板
control_panel = wx.Panel(self)
grid = wx.GridBagSizer(10, 10)
# IP和端口输入
grid.Add(wx.StaticText(control_panel, label="Destination IP:"), (0, 0), flag=wx.ALIGN_CENTER_VERTICAL)
self.ip_entry = wx.TextCtrl(control_panel, value="127.0.0.1")
grid.Add(self.ip_entry, (0, 1))
grid.Add(wx.StaticText(control_panel, label="Port:"), (0, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.port_entry = wx.TextCtrl(control_panel, value="8000", size=(80, -1))
grid.Add(self.port_entry, (0, 3))
# 消息输入
grid.Add(wx.StaticText(control_panel, label="Message:"), (1, 0), flag=wx.ALIGN_CENTER_VERTICAL)
self.msg_entry = wx.TextCtrl(control_panel, style=wx.TE_PROCESS_ENTER)
grid.Add(self.msg_entry, (1, 1), (1, 3), wx.EXPAND)
# 按钮
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.send_btn = wx.Button(control_panel, label="Send Once")
self.start_btn = wx.Button(control_panel, label="Start Periodic")
self.stop_btn = wx.Button(control_panel, label="Stop", enabled=False)
self.interval_entry = wx.TextCtrl(control_panel, value="1000", size=(60, -1))
btn_sizer.Add(self.send_btn, 0, wx.RIGHT, 5)
btn_sizer.Add(self.start_btn, 0, wx.RIGHT, 5)
btn_sizer.Add(self.stop_btn, 0, wx.RIGHT, 10)
btn_sizer.Add(wx.StaticText(control_panel, label="Interval (ms):"), 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5)
btn_sizer.Add(self.interval_entry, 0)
grid.Add(btn_sizer, (2, 0), (1, 4), wx.EXPAND|wx.TOP, 5)
control_panel.SetSizer(grid)
# 修复状态栏问题:确保字段数量匹配
self.status_bar = self.CreateStatusBar(len(self.ports))
status_widths = [-1] * len(self.ports)
self.status_bar.SetStatusWidths(status_widths)
# 主布局
main_sizer = wx.BoxSizer(wx.VERTICAL)
main_sizer.Add(self.notebook, 1, wx.EXPAND)
main_sizer.Add(control_panel, 0, wx.EXPAND|wx.ALL, 10)
self.SetSizer(main_sizer)
# 绑定事件
self.send_btn.Bind(wx.EVT_BUTTON, self.on_send)
self.start_btn.Bind(wx.EVT_BUTTON, self.on_start_periodic)
self.stop_btn.Bind(wx.EVT_BUTTON, self.on_stop_periodic)
self.msg_entry.Bind(wx.EVT_TEXT_ENTER, self.on_send)
self.notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_tab_change)
self.Bind(wx.EVT_CLOSE, self.on_close)
# 启动接收线程
for port in self.ports:
self.start_receiver(port)
# 定时器
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_periodic_send, self.timer)
# 状态更新定时器
self.stats_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.update_status, self.stats_timer)
self.stats_timer.Start(500)
def start_receiver(self, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', port))
sock.settimeout(0.1) # 设置超时防止永久阻塞
self.recv_socks[port] = sock
def listener():
while hasattr(self, 'recv_socks') and port in self.recv_socks:
try:
data, addr = sock.recvfrom(4096)
wx.CallAfter(self.process_data, port, data, addr)
except socket.timeout:
continue
except:
break
thread = threading.Thread(target=listener, daemon=True)
thread.start()
self.recv_threads[port] = thread
def process_data(self, port, data, addr):
try:
message = data.decode(errors='replace')
except:
message = data.hex()
self.rx_stats[port] += 1
self.tabs[port].add_message(f"{addr[0]}:{addr[1]} - {message}", "RX")
# 8005标签页显示所有接收和发送的消息
if port == 8005:
self.tabs[8005].add_message(f"{addr[0]}:{addr[1]} - {message}", "RX")
# 自动解析当前激活标签页的消息
current_page = self.notebook.GetPage(self.notebook.GetSelection())
if current_page.port == port:
current_page.parse_message(data)
def on_send(self, event):
self.send_message()
def on_start_periodic(self, event):
try:
interval = int(self.interval_entry.GetValue())
if interval < 100:
interval = 100
self.timer.Start(interval)
self.start_btn.Enable(False)
self.stop_btn.Enable(True)
except ValueError:
wx.MessageBox("Please enter a valid interval value", "Error", wx.OK|wx.ICON_ERROR)
def on_stop_periodic(self, event):
self.timer.Stop()
self.start_btn.Enable(True)
self.stop_btn.Enable(False)
def on_periodic_send(self, event):
self.send_message()
def send_message(self):
ip = self.ip_entry.GetValue().strip()
if not ip:
return
try:
port = int(self.port_entry.GetValue())
message = self.msg_entry.GetValue()
if not message:
return
self.send_sock.sendto(message.encode(), (ip, port))
self.tx_count += 1
# 更新8005标签页
timestamp = time.strftime('%H:%M:%S')
self.tabs[8005].add_message(f"{ip}:{port} - {message}", "TX")
except Exception as e:
wx.LogError(f"Send error: {str(e)}")
def on_tab_change(self, event):
"""Tab切换时恢复滚动并解析第一条消息"""
new_idx = event.GetSelection()
port = self.ports[new_idx]
self.tabs[port].paused = False
# 尝试解析当前标签的最后一条消息
if self.tabs[port].message_queue:
_, last_msg = self.tabs[port].message_queue[-1]
# 这里需要实际的数据,但为了示例我们跳过
def update_status(self, event):
"""修复状态栏字段匹配问题"""
status_texts = []
for idx, port in enumerate(self.ports):
if port == 8005:
text = f"Port{port}: Tx:{self.tx_count} Rx:{self.rx_stats.get(port,0)}"
else:
text = f"Port{port}: Rx:{self.rx_stats.get(port,0)}"
status_texts.append(text)
# 安全设置状态文本
if idx < self.status_bar.GetFieldsCount():
self.status_bar.SetStatusText(text, idx)
def on_close(self, event):
"""关闭时清理资源"""
if self.timer.IsRunning():
self.timer.Stop()
self.stats_timer.Stop()
# 关闭所有socket
for sock in self.recv_socks.values():
try:
sock.close()
except:
pass
try:
self.send_sock.close()
except:
pass
self.Destroy()
if __name__ == "__main__":
app = wx.App(False)
frame = UDPMonitorFrame()
frame.Show()
app.MainLoop()
```
### 关键修复说明
1. **状态栏字段匹配问题修复**:
```python
# 创建与端口数量匹配的状态栏字段
self.status_bar = self.CreateStatusBar(len(self.ports))
status_widths = [-1] * len(self.ports)
self.status_bar.SetStatusWidths(status_widths)
# 更新状态时确保索引在范围内
if idx < self.status_bar.GetFieldsCount():
self.status_bar.SetStatusText(text, idx)
```
2. **增强功能改进**:
- 发送消息间隔可配置
- 消息颜色区分(蓝色发送/绿色接收)
- 自动尝试多种解码格式
- 更健壮的错误处理
- 消息自动滚动优化
- 十六进制显示无法解码的消息
3. **其他优化**:
- 添加了Socket超时处理
- 改进消息解析逻辑
- 添加关闭事件处理
- 更友好的用户界面
- 更好的错误反馈
### 使用说明
1. **状态栏**:
- 每个端口对应一个状态栏字段
- 8005端口显示发送(Tx)和接收(Rx)统计
- 其他端口只显示接收统计
2. **消息显示**:
- 点击消息区域暂停/恢复自动滚动
- 切换Tab自动恢复滚动
- 发送消息显示为蓝色,接收为绿色
3. **协议解析**:
- 自动尝试Bit/TV/TLV格式解析
- 无法解析时显示十六进制和长度
- 树形结构展示解析结果
4. **发送控制**:
- 单次发送:点击"Send Once"或按回车
- 周期发送:设置间隔后点击"Start Periodic"
- 停止发送:点击"Stop"
### 扩展建议
1. **解码器扩展**:
继承`ProtocolDecoder`并重写`decode`方法添加自定义协议
2. **消息过滤**:
在`process_data`方法中添加过滤逻辑
3. **持久化**:
添加消息保存功能,使用SQLite或文件存储