Enumerate all fixed-width fonts

本文介绍如何使用EnumFontFamiliesEx函数枚举系统中所有固定宽度字体及其可用大小,包括设置LOGFONT结构体及编写回调函数的具体步骤。

Enumerating fonts can be a little confusing, and unless you want to enumerate all fonts on your system, can be a little more difficult than MSDN suggests. This article will explain exactly the steps you need to use to find every fixed-width font on your system, and also enumerate every possible size for each individual font.

Enumerate fonts

The best function to use for font enumeration is EnumFontFamiliesEx. You must set up a LOGFONT structure which tells EnumFontFamilesEx what fonts you wish to enumerate. You only need to specify three members of this structure.

The lfCharSet member indicates which character set you want to select. The DEFAULT_CHARSET value indicates that all character sets (i.e. OEM, ANSI, Chinese etc) will be enumerated. You could always specify ANSI_CHARSET, but in this case we want OEM character sets as well.

The lfPitchAndFamily member describes the look of a font in a general way. In this case, we don't mind what the font will look like (FF_DONTCARE), because we want to enumerate all fonts anyway, right? The FIXED_PITCH value just tells Windows that we are only interested in fixed-width fonts.

Finally, the lfFaceName member is used to tell windows what font we want to enumerate. If you specify the name of a string here, then Windows will enumerate the available font sizes for that font. First of all though, we want to enumerate the font names themselves, so we will just set this to an empty string. Here's the code.

int EnumFixedFonts(void)
{
    LOGFONT logfont;

    ZeroMemory(&logfont, sizeof logfont);

    logfont.lfCharSet = DEFAULT_CHARSET;
    logfont.lfPitchAndFamily = FIXED_PITCH | FF_DONTCARE;
    lstrcpy(logfont.lfFaceName, "/0");

    hdc = GetDC(0);

    EnumFontFamiliesEx(hdc, &logfont, (FONTENUMPROC)FontNameProc, 0, 0);

    ReleaseDC(0, hdc);

    return 0;
}

Before you can use this call though, you must write a callback function which Windows will call during the font enumeration. This callback procedure is called once for every font windows finds. Bear in mind that this procedure could be called several times for the same font, once for every character set. This is what the callback function should look like.

int CALLBACK FontNameProc(
    ENUMLOGFONTEX    *lpelfe,   /* pointer to logical-font data */
    NEWTEXTMETRICEX  *lpntme,   /* pointer to physical-font data */
    int              FontType,  /* type of font */
    LPARAM           lParam     /* a combo box HWND */
    )
{
    int i;

    if(lpelfe->elfLogFont.lfPitchAndFamily & FIXED_PITCH)
    {
        /* Make sure the fonts are only added once */
        for(i = 0; i < curfont; i++)
        {
            if(lstrcmp(currentfonts[i], (char *)lpelfe->elfFullName) == 0)
                return 1;
        }

        printf("%-16s: ", lpelfe->elfFullName);        
        cursize = 0;
        EnumFontSizes((char *)lpelfe->elfFullName);
        printf("/n");

        lstrcpy(currentfonts[curfont], (char *)lpelfe->elfFullName);
        
        if(++curfont == 200)
            return 0;
    
    }

    return 1;
}

An important part of the callback function is to check if the font has already been enumerated. In this case we keep a list of all fonts that have been enumerated so far. If the callback function executes and the font is already in the list of "processed fonts", then the function just returns 1 (or any non-zero value) to continue to the next font.

Now that we have a method to find every fixed-width font, we can enumerate all of the font sizes for each font.

Enumerate font sizes

Font size enumeration is almost the same as enumerating the fonts themselves. The only difference is that we specify the name of each font to get the sizes for. The following function enumerates all available font sizes for the specified font name

int EnumFontSizes(char *fontname)
{
    LOGFONT logfont;

    ZeroMemory(&logfont, sizeof logfont);

    logfont.lfHeight = 0;
    logfont.lfCharSet = DEFAULT_CHARSET;
    logfont.lfPitchAndFamily = FIXED_PITCH | FF_DONTCARE;
    lstrcpy(logfont.lfFaceName, fontname);

    EnumFontFamiliesEx(hdc, &logfont, (FONTENUMPROC)FontSizesProc, 0, 0);

    return 0;
}

By looking at the font name callback listed previously, you will see that this font size enumeration is executed for every unique font that we find. This is purely for the purpose of this example, as I wanted to display the available font sizes along side each font. In a normal Windows application, you would probably want to just store the font names in a drop-down list box, and whenever a font was selected from this list, fill up another list box with its available sizes.

int CALLBACK FontSizesProc(
    LOGFONT *plf,      /* pointer to logical-font data */
    TEXTMETRIC *ptm,   /* pointer to physical-font data */
    DWORD FontType,    /* font type */
    LPARAM lParam      /* pointer to application-defined data */
    )
{
    static int truetypesize[] = { 8, 9, 10, 11, 12, 14, 16, 18, 20, 
            22, 24, 26, 28, 36, 48, 72 };


    int i;

    if(FontType != TRUETYPE_FONTTYPE)
    {
        int  logsize    = ptm->tmHeight - ptm->tmInternalLeading;
        long pointsize  = MulDiv(logsize, 72, GetDeviceCaps(hdc, LOGPIXELSY));

        for(i = 0; i < cursize; i++)
            if(currentsizes[i] == pointsize) return 1;

        printf("%d ", pointsize);
        
        currentsizes[cursize] = pointsize;
        if(++cursize == 200) return 0;
        
        return 1;   
    }
    else
    {

        for(i = 0; i < (sizeof(truetypesize) / sizeof(truetypesize[0])); i++)
        {
            printf("%d ", truetypesize[i]);
        }

        return 0;
    }

}

An important thing to note for the font-size callback function (above) is the test to see if the font is true-type or not. If it is, then all that is necessary is to print a list of pre-determined point sizes which are deamed suitable. In this case, the callback will only be executed once for a true-type font. However, if the font is a raster font, then the callback will be executed once for every size that the font supports. It is our job to convert the size of the font from logical units into point sizes, which are generally more useful if they are to be presented to a user.

Thats it. All that is needed now is to start off the enumeration!

int main(void)
{
    EnumFixedFonts();
    return 0;
}

Below is the output of the program on my system, which currently has six fixed-width fonts installed. Notice that three of the fonts are obviously raster fonts, and have a limited number of sizes, whereas the Courier New, Lucida and Andale fonts are true-type.

Terminal        : 9 5 6 14 12
Fixedsys        : 9
Courier         : 10 12 15
Courier New     : 8 9 10 11 12 14 16 18 20 22 24 26 28 36 48 72
Lucida Console  : 8 9 10 11 12 14 16 18 20 22 24 26 28 36 48 72
Andale Mono     : 8 9 10 11 12 14 16 18 20 22 24 26 28 36 48 72
 
# 导入必要库 import pandas as pd import matplotlib import matplotlib.pyplot as plt import matplotlib.font_manager as fm import logging import sys import os from matplotlib.patches import Patch # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger('PowerBI_YoY_Chart') # 配置中文字体支持 def configure_chinese_font(): """配置中文字体支持""" try: # 尝试加载常用中文字体 font_names = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS', 'WenQuanYi Micro Hei', 'SimSun'] available_fonts = [f.name for f in fm.fontManager.ttflist] # 记录可用字体 logger.info(f"可用字体: {', '.join(available_fonts[:10])}...") for font_name in font_names: if font_name in available_fonts: plt.rcParams['font.family'] = font_name logger.info(f"使用中文字体: {font_name}") return True # 尝试加载Windows系统字体 if sys.platform == 'win32': font_paths = [ r'C:\Windows\Fonts\simhei.ttf', # 黑体 r'C:\Windows\Fonts\msyh.ttc', # 微软雅黑 r'C:\Windows\Fonts\simsun.ttc', # 宋体 r'C:\Windows\Fonts\STSONG.TTF' # 华文宋体 ] for font_path in font_paths: if os.path.exists(font_path): font_prop = fm.FontProperties(fname=font_path) plt.rcParams['font.family'] = font_prop.get_name() logger.info(f"使用系统字体: {font_path}") return True # 使用默认字体 plt.rcParams['font.family'] = ['sans-serif'] logger.warning("未找到中文字体,可能无法正确显示中文") return False except Exception as e: logger.error(f"字体配置错误: {str(e)}") return False # 配置中文字体 configure_chinese_font() plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 def sort_months(months): """对月份进行排序:英文月份→数字月份→Average""" month_order = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12, 'January': 1, 'February': 2, 'March': 3, 'April': 4, 'May': 5, 'June': 6, 'July': 7, 'August': 8, 'September': 9, 'October': 10, 'November': 11, 'December': 12 } numeric_months = [] en_month_list = [] average_month = None for month in months: if pd.isna(month): continue str_month = str(month).strip() if str_month.lower() == 'average': average_month = 'Average' elif str_month in month_order: en_month_list.append(str_month) elif str_month.isdigit(): numeric_months.append(int(str_month)) else: # 尝试匹配其他格式的月份 str_month_lower = str_month.lower() if str_month_lower.startswith('jan'): en_month_list.append('Jan') elif str_month_lower.startswith('feb'): en_month_list.append('Feb') elif str_month_lower.startswith('mar'): en_month_list.append('Mar') elif str_month_lower.startswith('apr'): en_month_list.append('Apr') elif str_month_lower.startswith('may'): en_month_list.append('May') elif str_month_lower.startswith('jun'): en_month_list.append('Jun') elif str_month_lower.startswith('jul'): en_month_list.append('Jul') elif str_month_lower.startswith('aug'): en_month_list.append('Aug') elif str_month_lower.startswith('sep'): en_month_list.append('Sep') elif str_month_lower.startswith('oct'): en_month_list.append('Oct') elif str_month_lower.startswith('nov'): en_month_list.append('Nov') elif str_month_lower.startswith('dec'): en_month_list.append('Dec') else: # 无法识别的月份原样保留 en_month_list.append(str_month) # 排序并拼接结果 en_month_list.sort(key=lambda x: month_order.get(x, month_order.get(x.capitalize(), 13))) numeric_months.sort() sorted_months = en_month_list + [str(m) for m in numeric_months] if average_month: sorted_months.append(average_month) return sorted_months # 主绘图函数 def plot_yoy_chart(dataset): """生成年度同比变化图表""" try: # 记录开始时间 logger.info("开始生成图表...") # 创建图表(增大图表尺寸以适应更大字体) fig, ax = plt.subplots(figsize=(14, 8)) # 复制数据集以防修改原始数据 df = dataset.copy() # 记录初始数据行数 logger.info(f"原始数据行数: {len(df)}") # 检查必需列是否存在 required_columns = ['Month', 'QTY', 'Year', 'Percentage'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: error_msg = f"缺少必需列: {', '.join(missing_columns)}" logger.error(error_msg) ax.text(0.5, 0.5, error_msg, ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 清洗数据 df = df.dropna(subset=required_columns) df = df[df['QTY'] > 0] # 移除无效QTY # 转换数据类型 df['Year'] = pd.to_numeric(df['Year'], errors='coerce').astype('Int64') df['Percentage'] = pd.to_numeric(df['Percentage'], errors='coerce') df['QTY'] = pd.to_numeric(df['QTY'], errors='coerce') df = df.dropna(subset=['Year', 'Percentage', 'QTY']) logger.info(f"清洗后数据行数: {len(df)}") # 检查是否有有效数据 if df.empty: ax.text(0.5, 0.5, '没有有效数据', ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 获取年份数据并检查 years = sorted(df['Year'].unique()) if len(years) < 2: ax.text(0.5, 0.5, '需要至少两年数据才能生成图表', ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 选取最近的两年数据 prev_year, curr_year = years[-2], years[-1] all_months = sort_months(df['Month'].unique()) logger.info(f"排序后的月份: {all_months}") # 准备绘图数据 data = { 'months': [], 'prev_qty': [], 'curr_qty': [], 'diff_percent': [], 'is_average': [] # 标记是否为Average } for month in all_months: prev_data = df[(df['Year'] == prev_year) & (df['Month'] == month)] curr_data = df[(df['Year'] == curr_year) & (df['Month'] == month)] if not prev_data.empty and not curr_data.empty: data['months'].append(month) data['prev_qty'].append(prev_data['QTY'].values[0]) data['curr_qty'].append(curr_data['QTY'].values[0]) # 计算百分比差异 prev_p = prev_data['Percentage'].values[0] curr_p = curr_data['Percentage'].values[0] diff_p = (curr_p - prev_p) * 100 data['diff_percent'].append(diff_p) # 标记是否为Average data['is_average'].append(str(month).strip().lower() == 'average') # 检查是否有共同月份数据 if not data['months']: ax.text(0.5, 0.5, f'{prev_year} 和 {curr_year} 无共同月份数据', ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 创建子图并绘制柱状图 width = 0.35 x_pos = range(len(data['months'])) # 绘制两年数据的柱状图(区分Average组) for i, (month, prev_qty, curr_qty) in enumerate(zip(data['months'], data['prev_qty'], data['curr_qty'])): is_avg = data['is_average'][i] # 设置颜色:Average组使用紫色,其他组使用默认颜色 prev_color = '#C04F15' if is_avg else '#00B0F0' # 紫色 vs 蓝色 118DFF curr_color = '#F6C6AD' if is_avg else '#A0D1FF' # 浅紫色 vs 橙色C04F15 # 绘制柱状图 prev_bar = ax.bar( x_pos[i] - width/2, prev_qty, width, label=f'{prev_year}' if i == 0 else None, color=prev_color ) curr_bar = ax.bar( x_pos[i] + width/2, curr_qty, width, label=f'{curr_year}' if i == 0 else None, color=curr_color ) # 为柱状图添加数值标签(增大字体) for bar in [prev_bar, curr_bar]: height = bar[0].get_height() ax.text( bar[0].get_x() + bar[0].get_width()/2, height * 1.02, f'{int(height):,}', ha='center', va='bottom', fontweight='bold', fontsize=10 ) # 计算所有柱子的最大高度,用于比例计算 max_qty = max(max(data['prev_qty']), max(data['curr_qty'])) # 调整 Y 轴范围,预留足够空间(增加顶部空间以容纳上移的元素) ax.set_ylim(0, max_qty * 1.6) # 增加更多顶部空间 # 固定文本偏移量(增大值使文本上移更多) fixed_text_offset = max_qty * 0.06 # 增加偏移量 # U形框与柱子顶部标签的间距,解决重叠问题 frame_offset = max_qty * 0.05 # 遍历每个月份,绘制U形虚线框、箭头、百分比文本 for i in range(len(data['months'])): # 获取当前组的两根柱子高度 prev_height = data['prev_qty'][i] # 左侧柱子高度 curr_height = data['curr_qty'][i] # 右侧柱子高度 # 计算U形框位置参数 frame_left = x_pos[i] - width/2 frame_right = x_pos[i] + width/2 # 调整U形框与柱子的间距,避免与顶部数字重叠 base_top = max(prev_height, curr_height) + frame_offset # U 形框竖线高度(略微减小,为上移的箭头留出空间) vert_height = max_qty * 0.08 # 稍微减小竖线高度 left_top = base_top + vert_height right_top = base_top + vert_height # U 形框顶部横线的 y 坐标(保持水平) top_line_y = max(left_top, right_top) # 绘制U形虚线框 # 左侧竖线:从柱子顶部上方开始 ax.plot([frame_left, frame_left], [prev_height + frame_offset, left_top], '--', color='gray', linewidth=1.0) # 右侧竖线:从柱子顶部上方开始 ax.plot([frame_right, frame_right], [curr_height + frame_offset, right_top], '--', color='gray', linewidth=1.0) # 顶部横线(连接左右竖线顶端) ax.plot([frame_left, frame_right], [top_line_y, top_line_y], '--', color='gray', linewidth=1.0) # 计算箭头和文本的位置(显著增加垂直间距,使元素上移) arrow_y = top_line_y + max_qty * 0.05 # 箭头位置大幅上移 text_y = arrow_y + fixed_text_offset # 文本位置基于调整后的箭头位置 center_x = (frame_left + frame_right) / 2 # 水平居中 # 根据百分比设置颜色和标记 diff_p = data['diff_percent'][i] if diff_p > 0: arrow_color = '#d62728' # 红色 marker = '^' # 向上三角形 marker_size = 80 elif diff_p < 0: arrow_color = '#2ca02c' # 绿色 marker = 'v' # 向下三角形 marker_size = 80 else: arrow_color = '#7f7f7f' # 灰色 marker = '_' # 水平线标记 marker_size = 80 # 绘制箭头 ax.scatter( center_x, arrow_y, marker=marker, color=arrow_color, s=marker_size, zorder=5 ) # 绘制百分比文本(增大字体并上移) text_color = arrow_color ax.text( center_x, text_y, f'{diff_p:+.1f}%', ha='center', va='bottom', fontsize=11, fontweight='bold', color=text_color ) # 设置图表属性(特别加大X轴字体) ax.set_xlabel('Month', fontsize=20) ax.set_ylabel('QTY', fontsize=20) # 增大Y轴标签字体到20 ax.set_title('YoY OTS QTY Change', fontsize=28, pad=20) ax.set_xticks(x_pos) ax.set_xticklabels(data['months'], fontsize=15, rotation=45, ha='right') # 设置网格线 - 虚线,只显示Y轴网格线,并与刻度对齐 ax.grid(True, axis='y', linestyle='--', linewidth=0.8, alpha=0.7) ax.grid(False, axis='x') # 不显示X轴网格线 # 设置Y轴刻度标签字体大小 ax.tick_params(axis='y', labelsize=14) # 添加自定义图例并放置在右上角 legend_elements = [ Patch(facecolor='#00B0F0', label=f'{prev_year}'), Patch(facecolor='#A0D1FF', label=f'{curr_year}') ] ax.legend( handles=legend_elements, fontsize=14, loc='upper right', # 将图例放置在右上角 frameon=False # 去除图例外框 ) # 去除所有图表边框 for spine in ['top', 'right', 'left', 'bottom']: ax.spines[spine].set_visible(False) # 重新显示底部边框(X轴) ax.spines['bottom'].set_visible(True) ax.spines['bottom'].set_color('#cccccc') # 设置为浅灰色 # 调整布局(增加顶部和底部空间) plt.subplots_adjust(top=0.88, bottom=0.15) plt.tight_layout() logger.info("图表生成成功") return plt except Exception as e: # 记录详细错误信息 logger.exception("图表生成过程中发生错误") # 创建错误信息图表 plt.figure(figsize=(8, 4)) error_msg = f"图表生成错误: {str(e)}" plt.text(0.5, 0.5, error_msg, ha='center', va='center', fontsize=14, color='red') plt.tight_layout() return plt # 在Power BI中执行的主函数 def main(dataset): """Power BI 入口函数""" try: # 检查数据集 if dataset is None or dataset.empty: plt.figure(figsize=(8, 4)) plt.text(0.5, 0.5, "没有提供数据", ha='center', va='center', fontsize=16) plt.tight_layout() plt.show() return # 生成图表 plot = plot_yoy_chart(dataset) # 显示图表 plot.show() except Exception as e: logger.exception("主函数执行错误") plt.figure(figsize=(8, 4)) plt.text(0.5, 0.5, f"执行错误: {str(e)}", ha='center', va='center', fontsize=14, color='red') plt.tight_layout() plt.show() # 执行主函数(Power BI会自动提供dataset变量) main(dataset) 以上代码,我需要将百分比数相减结果是0时,在箭头上方的百分比数前不要显示“+”号,请按我的需求改好,并给出完整代码。
08-09
import cv2 import numpy as np import math from collections import deque from ultralytics import YOLO import time import os try: from PIL import ImageFont, ImageDraw, Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False # 关键点索引定义 KEYPOINT_INDICES = { "left_shoulder": 5, "right_shoulder": 6, "left_elbow": 7, "right_elbow": 8, "left_wrist": 9, "right_wrist": 10, "left_hip": 11, "right_hip": 12, "left_ear": 3, "right_ear": 4 } def draw_stability_bar(panel, x, stability, color): """绘制稳定性进度条""" bar_width = 60 fill_width = int(bar_width * stability / 100) cv2.rectangle(panel, (x, 20 - 10), (x + bar_width, 20 + 5), (100, 100, 100), -1) cv2.rectangle(panel, (x, 20 - 10), (x + fill_width, 20 + 5), color, -1) stability_text = f"{stability:.0f}%" cv2.putText(panel, stability_text, (x + bar_width + 5, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) class DumbbellCurlAnalyzer: def __init__(self, model_path='yolov8s-pose.pt', display_width=1280, display_height=720): """初始化哑铃弯举分析器""" self.display_width = display_width self.display_height = display_height # 尝试加载模型 try: print(f"正在加载模型: {model_path}") self.model = YOLO(model_path) test_img = np.zeros((640, 640, 3), dtype=np.uint8) self.model.predict(test_img, verbose=False) print("模型加载成功") except Exception as e: raise RuntimeError(f"模型加载失败: {str(e)}") # 性能优化参数 self.skip_counter = 0 self.skip_interval = 2 # 每3帧处理1帧 self.last_results = None # 结果缓存 # 多人状态跟踪 self.max_persons = 3 self.person_states = [] # 动态创建状态 # 颜色映射 self.person_colors = [ (0, 255, 0), # 绿色 (0, 165, 255), # 橙色 (0, 0, 255) # 红色 ] # 角度阈值 self.min_angle = 0 self.max_angle = 150 # 代偿参数 self.compensation_threshold = 0.05 # 身高的5% self.compensation_sensitivity = 0.6 # 帧率跟踪 self.prev_frame_time = 0 self.current_fps = 30 self.fps_smoother = deque(maxlen=5) # 中文支持 self.PIL_AVAILABLE = PIL_AVAILABLE self.DEFAULT_FONT_PATH = self._find_font_path() # 初始化历史计数 self.counter_history = {} def _find_font_path(self): """查找系统中可用的中文字体""" possible_fonts = [ "C:/Windows/Fonts/simsun.ttc", # Windows 宋体 "C:/Windows/Fonts/simhei.ttf", # Windows 黑体 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", # Linux 文泉驿微米黑 "/System/Library/Fonts/PingFang.ttc", # macOS 苹方 "/Library/Fonts/SimHei.ttf" # macOS 黑体 ] for font_path in possible_fonts: if os.path.exists(font_path): print(f"找到中文字体: {font_path}") return font_path print("警告: 未找到中文字体,中文可能无法正常显示") return None def calculate_angle(self, a, b, c): """计算三点夹角(B为顶点)""" ba = [a[0] - b[0], a[1] - b[1]] bc = [c[0] - b[0], c[1] - b[1]] dot_product = ba[0] * bc[0] + ba[1] * bc[1] magnitude_ba = math.sqrt(ba[0] ** 2 + ba[1] ** 2) magnitude_bc = math.sqrt(bc[0] ** 2 + bc[1] ** 2) if magnitude_ba * magnitude_bc == 0: return 0 cosine = dot_product / (magnitude_ba * magnitude_bc) cosine = max(-1.0, min(1.0, cosine)) return math.degrees(math.acos(cosine)) def detect_compensation(self, keypoints_dict, side, person_state): """检测肩部代偿动作""" shoulder = f"{side}_shoulder" ear = f"{side}_ear" compensation_types = [] confidence = 0 # 身高估计 ref_distance = self.get_reference_distance(keypoints_dict) if ref_distance < 10: return compensation_types, confidence # 肩部位移检测 if shoulder in keypoints_dict: tracker = person_state["shoulder_trackers"][side] current_pos = keypoints_dict[shoulder] # 添加速度计算 avg_speed = 0 if len(tracker["path"]) > 0: # 计算最近5帧的平均移动速度 speeds = [] for i in range(1, min(5, len(tracker["path"]))): dx = tracker["path"][-i][0] - tracker["path"][-i - 1][0] dy = tracker["path"][-i][1] - tracker["path"][-i - 1][1] speed = math.sqrt(dx ** 2 + dy ** 2) / ref_distance speeds.append(speed) if speeds: avg_speed = sum(speeds) / len(speeds) # 速度阈值-小于此值视为静止 SPEED_THRESHOLD = 0.005 # 身高的0.5% if tracker["previous"]: dx = current_pos[0] - tracker["previous"][0] dy = current_pos[1] - tracker["previous"][1] # 相对位移计算 relative_dx = dx / ref_distance relative_dy = dy / ref_distance # 只有当速度超过阈值时才进行代偿检测 if avg_speed > SPEED_THRESHOLD: if abs(relative_dx) > self.compensation_threshold or abs(relative_dy) > self.compensation_threshold: compensation_types.append(f"shoulder_displacement_{side}") confidence += 0.4 # 耸肩检测(相对位移dy为负表示向上) if relative_dy < -self.compensation_threshold: compensation_types.append(f"shoulder_elevation_{side}") confidence += 0.3 # 更新代偿计数 if relative_dx > self.compensation_threshold or relative_dy < -self.compensation_threshold: tracker["compensation_count"] = min(10, tracker["compensation_count"] + 1) else: tracker["compensation_count"] = max(0, tracker["compensation_count"] - 2) else: # 静止状态下减少代偿计数 tracker["compensation_count"] = max(0, tracker["compensation_count"] - 3) # 更新历史位置 tracker["previous"] = current_pos tracker["path"].append(current_pos) else: # 第一次检测到,初始化previous tracker["previous"] = current_pos tracker["path"].append(current_pos) # 连续代偿增强置信度 if "shoulder_trackers" in person_state and side in person_state["shoulder_trackers"]: tracker = person_state["shoulder_trackers"][side] if tracker["compensation_count"] > 3: confidence += min(0.3, tracker["compensation_count"] * 0.1) # 肩耳相对位置检测-仅当有移动时才检测 if avg_speed > SPEED_THRESHOLD and shoulder in keypoints_dict and ear in keypoints_dict: shoulder_y = keypoints_dict[shoulder][1] ear_y = keypoints_dict[ear][1] elevation_ratio = (ear_y - shoulder_y) / ref_distance if elevation_ratio < 0.25: compensation_types.append(f"shoulder_elevation_{side}") confidence += max(0.3, (0.25 - elevation_ratio) * 2) return compensation_types, min(1.0, confidence) def get_reference_distance(self, keypoints_dict): """估计身高作为参考""" if "left_shoulder" in keypoints_dict and "right_shoulder" in keypoints_dict: left = keypoints_dict["left_shoulder"] right = keypoints_dict["right_shoulder"] shoulder_width = math.sqrt((left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2) return shoulder_width * 4 # 肩宽×4估计身高 elif "left_hip" in keypoints_dict and "right_hip" in keypoints_dict: left = keypoints_dict["left_hip"] right = keypoints_dict["right_hip"] hip_width = math.sqrt((left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2) return hip_width * 3 # 髋宽×3估计身高 return 0 def analyze_motion_phase(self, side, person_state): """判断动作阶段(上举/下落/保持)""" angles = list(person_state["history_angles"][side]) if len(angles) < 5: return "UNKNOWN" # 计算速度 velocity = np.mean(np.diff(angles[-5:])) if len(angles) >= 5 else 0 if velocity > 7: return "LIFTING" elif velocity < -7: return "LOWERING" else: return "HOLDING" def interpolate_point(self, previous_point, current_pos, max_distance=100): """关键点缺失时插值""" if previous_point is None: return current_pos dx = current_pos[0] - previous_point[0] dy = current_pos[1] - previous_point[1] distance = math.sqrt(dx ** 2 + dy ** 2) if distance > max_distance: return current_pos return previous_point def get_or_create_person_state(self, center): """获取或创建人员状态""" # 如果状态列表为空,直接创建第一个状态 if not self.person_states: return self.create_new_person_state(center) # 寻找最近的现有状态 min_dist = float('inf') closest_idx = None for i, state in enumerate(self.person_states): if state["last_position"]: dist = math.sqrt( (center[0] - state["last_position"][0]) ** 2 + (center[1] - state["last_position"][1]) ** 2 ) if dist < min_dist: min_dist = dist closest_idx = i # 如果没有足够近的现有状态,创建新状态 if min_dist > 100 or closest_idx is None: if len(self.person_states) < self.max_persons: return self.create_new_person_state(center) else: # 已满,返回最旧的状态 return self.person_states[0], 0 # 更新最近状态的位置 self.person_states[closest_idx]["last_position"] = center return self.person_states[closest_idx], closest_idx def create_new_person_state(self, center): """创建新的人员状态""" new_state = { "history_angles": { "left": deque(maxlen=15), "right": deque(maxlen=15) }, "shoulder_trackers": { "left": {"path": deque(maxlen=30), "previous": None, "compensation_count": 0}, "right": {"path": deque(maxlen=30), "previous": None, "compensation_count": 0} }, "prev_keypoints": { "left": {"shoulder": None, "elbow": None, "wrist": None}, "right": {"shoulder": None, "elbow": None, "wrist": None} }, "missing_frames": { "left": {"shoulder": 0, "elbow": 0, "wrist": 0}, "right": {"shoulder": 0, "elbow": 0, "wrist": 0} }, "counter": {"left": 0, "right": 0}, "counter_state": {"left": "down", "right": "down"}, "last_position": center } self.person_states.append(new_state) return new_state, len(self.person_states) - 1 def analyze_frame(self, frame): """分析单帧图像""" # 帧率计算 current_time = time.time() if self.prev_frame_time > 0: self.current_fps = 1 / (current_time - self.prev_frame_time) self.prev_frame_time = current_time # 平滑帧率 self.fps_smoother.append(self.current_fps) if len(self.fps_smoother) > 0: smoothed_fps = sum(self.fps_smoother) / len(self.fps_smoother) else: smoothed_fps = self.current_fps # 跳帧处理 self.skip_counter = (self.skip_counter + 1) % (self.skip_interval + 1) if self.skip_counter != 0 and self.last_results is not None: return self.last_results # 调整帧大小以匹配显示尺寸 frame = cv2.resize(frame, (self.display_width, self.display_height)) # 姿态估计 results = self.model(frame, verbose=False) # 结果初始化 analysis_results = { "fps": smoothed_fps, "persons": [] # 存储每个人的结果 } try: # 动态置信度阈值 conf_threshold = max(0.2, min(0.7, 0.5 * (smoothed_fps / 30))) if len(results) == 0 or results[0].keypoints is None: return analysis_results, frame boxes = results[0].boxes.xyxy.cpu().numpy() kpts = results[0].keypoints.data.cpu().numpy() if len(boxes) == 0: return analysis_results, frame # 根据面积排序 areas = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) sorted_idxs = np.argsort(areas)[::-1][:self.max_persons] # 取面积最大的三个 # 处理每个人 for idx in sorted_idxs: kpts_data = kpts[idx] box = boxes[idx] center = ((box[0] + box[2]) / 2, (box[1] + box[3]) / 2) # 获取或创建对应的状态 person_state, person_id = self.get_or_create_person_state(center) # 提取关键点 keypoints_dict = {} for part, idx_kpt in KEYPOINT_INDICES.items(): if idx_kpt < len(kpts_data): x, y, conf = kpts_data[idx_kpt] if conf > conf_threshold: keypoints_dict[part] = (int(x), int(y)) side, point = part.split('_', 1) if point in person_state["missing_frames"][side]: if conf > conf_threshold: person_state["missing_frames"][side][point] = 0 else: person_state["missing_frames"][side][point] += 1 # 关键点插值 for side in ["left", "right"]: for point in ["shoulder", "elbow", "wrist"]: key_name = f"{side}_{point}" if key_name not in keypoints_dict and person_state["prev_keypoints"][side][point] is not None: if person_state["missing_frames"][side][point] < 15: # 最大插值帧数 keypoints_dict[key_name] = self.interpolate_point( person_state["prev_keypoints"][side][point], person_state["prev_keypoints"][side][point] ) # 更新历史关键点 for side in ["left", "right"]: for point in ["shoulder", "elbow", "wrist"]: key_name = f"{side}_{point}" prev_key = person_state["prev_keypoints"][side][point] if key_name in keypoints_dict: person_state["prev_keypoints"][side][point] = keypoints_dict[key_name] else: person_state["prev_keypoints"][side][point] = prev_key # 初始化个人结果 person_result = { "id": person_id, "color": self.person_colors[person_id % len(self.person_colors)], "left_angle": None, "right_angle": None, "left_feedback": "", "right_feedback": "", "left_compensation": [], "right_compensation": [], "left_compensation_confidence": 0, "right_compensation_confidence": 0, "left_phase": "UNKNOWN", "right_phase": "UNKNOWN", "left_count": person_state["counter"]["left"], "right_count": person_state["counter"]["right"], "box": box, "keypoints": keypoints_dict } # 更新历史计数 person_key = f"person_{person_id}" if person_key not in self.counter_history: self.counter_history[person_key] = [] self.counter_history[person_key].append( person_state["counter"]["left"] + person_state["counter"]["right"]) # 分析左右手臂 for side in ["left", "right"]: shoulder = f"{side}_shoulder" elbow = f"{side}_elbow" wrist = f"{side}_wrist" if shoulder in keypoints_dict and elbow in keypoints_dict and wrist in keypoints_dict: # 计算角度 angle = self.calculate_angle( keypoints_dict[shoulder], keypoints_dict[elbow], keypoints_dict[wrist] ) # 添加到历史 person_state["history_angles"][side].append(angle) person_result[f"{side}_angle"] = angle # 动作阶段 phase = self.analyze_motion_phase(side, person_state) person_result[f"{side}_phase"] = phase # 反馈信息 feedback = "" if angle < self.min_angle: feedback = "手臂过度伸展!" elif angle > self.max_angle: feedback = "弯曲角度过大!" else: feedback = "动作规范" feedback += f"|{phase}" # 代偿检测 compensations, confidence = self.detect_compensation(keypoints_dict, side, person_state) person_result[f"{side}_compensation"] = compensations person_result[f"{side}_compensation_confidence"] = confidence # 代偿反馈 if confidence > self.compensation_sensitivity: if f"shoulder_displacement_{side}" in compensations: feedback += "|肩部不稳定!" if f"shoulder_elevation_{side}" in compensations: feedback += "|避免耸肩!" person_result[f"{side}_feedback"] = feedback # 动作计数 if angle < 40 and person_state["counter_state"][side] == "down": person_state["counter_state"][side] = "up" elif angle > 120 and person_state["counter_state"][side] == "up": person_state["counter"][side] += 1 person_state["counter_state"][side] = "down" person_result[f"{side}_count"] = person_state["counter"][side] else: person_result[f"{side}_feedback"] = f"{side}关键点未检测到" analysis_results["persons"].append(person_result) # 可视化 viz_frame = self.visualize_feedback(frame, analysis_results) except Exception as e: print(f"分析错误: {str(e)}") viz_frame = frame analysis_results["persons"] = [] self.last_results = (analysis_results, viz_frame) return analysis_results, viz_frame def visualize_feedback(self, frame, analysis_results): """可视化分析结果""" viz_frame = frame.copy() height, width = frame.shape[:2] # 绘制人物信息(骨架、关键点等) for person in analysis_results["persons"]: color = person["color"] # 绘制边界框 box = person["box"] cv2.rectangle(viz_frame, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), color, 2) # 绘制ID cv2.putText(viz_frame, f"ID:{person['id']}", (int(box[0]), int(box[1]) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) # 显示计数信息 count_text = f"L:{person['left_count']} R:{person['right_count']}" cv2.putText(viz_frame, count_text, (int(box[0]), int(box[1]) + 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # 关键点可视化 for person in analysis_results["persons"]: color = person["color"] keypoints_dict = person.get("keypoints", {}) # 获取对应的person_state if person["id"] < len(self.person_states): person_state = self.person_states[person["id"]] else: continue # 跳过无法找到状态的人 # 绘制身体骨架 (简化版) skeleton_pairs = [ ("left_shoulder", "left_elbow"), ("left_elbow", "left_wrist"), ("right_shoulder", "right_elbow"), ("right_elbow", "right_wrist"), ("left_shoulder", "right_shoulder"), ("left_shoulder", "left_hip"), ("right_shoulder", "right_hip"), ("left_hip", "right_hip") ] for start, end in skeleton_pairs: if start in keypoints_dict and end in keypoints_dict: cv2.line(viz_frame, keypoints_dict[start], keypoints_dict[end], color, 2) # 绘制关节点 for point in keypoints_dict.values(): cv2.circle(viz_frame, point, 5, color, -1) # 绘制手臂角度 for side in ["left", "right"]: elbow_key = f"{side}_elbow" if person[f"{side}_angle"] and elbow_key in keypoints_dict: angle = person[f"{side}_angle"] position = (keypoints_dict[elbow_key][0] + 10, keypoints_dict[elbow_key][1] - 10) cv2.putText(viz_frame, f"{angle:.0f}°", position, cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # 实时反馈信息板 feedback_height = 120 feedback_panel = np.zeros((feedback_height, width, 3), dtype=np.uint8) feedback_panel[:] = (40, 40, 60) y_offset = 20 for person in analysis_results["persons"]: color = person["color"] id_text = f"ID {person['id']}:" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: feedback_panel = self.put_chinese_text(feedback_panel, id_text, (20, y_offset), color, 22) else: cv2.putText(feedback_panel, id_text, (20, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) # 左右手臂反馈信息 for side in ["left", "right"]: feedback_text = f"{side}臂: {person[f'{side}_feedback']}" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: feedback_panel = self.put_chinese_text(feedback_panel, feedback_text, (100, y_offset), color, 18) else: cv2.putText(feedback_panel, feedback_text, (100, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1) y_offset += 25 # 代偿信息 compensations = person["left_compensation"] + person["right_compensation"] if compensations: compensation_text = "代偿: " + ", ".join(compensations) if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: feedback_panel = self.put_chinese_text(feedback_panel, compensation_text, (100, y_offset), (0, 0, 255), 18) else: cv2.putText(feedback_panel, compensation_text, (100, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1) y_offset += 25 y_offset += 10 # 人员间间隔 # 叠加反馈面板到右侧 feedback_width = min(400, width // 3) viz_frame[0:feedback_height, -feedback_width:] = cv2.addWeighted( viz_frame[0:feedback_height, -feedback_width:], 0.2, feedback_panel[:, -feedback_width:], 0.8, 0 ) # 绘制固定的数据框(顶部面板) self.draw_fixed_data_panel(viz_frame, analysis_results) return viz_frame def draw_fixed_data_panel(self, frame, analysis_results): """绘制固定在顶部的数据面板""" panel_height = 120 # 增加面板高度以容纳更多内容 panel_width = frame.shape[1] panel = np.zeros((panel_height, panel_width, 3), dtype=np.uint8) panel[:] = (40, 40, 60) # 深蓝灰色背景 # 标题 title = "多人哑铃弯举分析" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, title, (20, 30), (0, 200, 255), 28) else: cv2.putText(panel, title, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 200, 255), 2) # 列标题和数据 headers = ["ID", "左计数", "右计数", "左肩稳定", "右肩稳定"] col_positions = [100, 200, 300, 420, 540] header_y = 60 # 标题行位置 data_y = 90 # 数据行位置 # 绘制列标题 for i, header in enumerate(headers): if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, header, (col_positions[i], header_y), (0, 255, 255), 20) else: cv2.putText(panel, header, (col_positions[i], header_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 1) # 绘制每个人的数据 for i, person in enumerate(analysis_results["persons"]): if i >= 3: # 只显示前3个人 break color = person["color"] x_offset = i * 200 # 为多个人物设置水平偏移 # ID if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, str(person["id"]), (col_positions[0] + x_offset, data_y), color, 20) else: cv2.putText(panel, str(person["id"]), (col_positions[0] + x_offset, data_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) # 左计数 if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, str(person["left_count"]), (col_positions[1] + x_offset, data_y), color, 20) else: cv2.putText(panel, str(person["left_count"]), (col_positions[1] + x_offset, data_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) # 右计数 if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, str(person["right_count"]), (col_positions[2] + x_offset, data_y), color, 20) else: cv2.putText(panel, str(person["right_count"]), (col_positions[2] + x_offset, data_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) # 左肩稳定 left_stability = max(0, min(100, 100 - person["left_compensation_confidence"] * 100)) draw_stability_bar(panel, col_positions[3] + x_offset, left_stability, color) # 右肩稳定 right_stability = max(0, min(100, 100 - person["right_compensation_confidence"] * 100)) draw_stability_bar(panel, col_positions[4] + x_offset, right_stability, color) # 添加帧率信息 fps_text = f"FPS: {analysis_results['fps']:.1f}" cv2.putText(panel, fps_text, (panel_width - 150, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) # 添加操作提示 hint_text = "ESC退出 | F全屏 | S截图 | Q切换质量" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, hint_text, (panel_width - 350, panel_height - 15), (200, 200, 200), 18) else: cv2.putText(panel, hint_text, (panel_width - 350, panel_height - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1) # 叠加面板到顶部 frame[0:panel_height, 0:panel_width] = cv2.addWeighted( frame[0:panel_height, 0:panel_width], 0.3, panel, 0.7, 0 ) def put_chinese_text(self, img, text, pos, color, font_size): """在图像上绘制中文文本""" if not self.PIL_AVAILABLE or self.DEFAULT_FONT_PATH is None: # 如果无法使用PIL或未找到字体,尝试使用默认字体 cv2.putText(img, text, pos, cv2.FONT_HERSHEY_SIMPLEX, font_size / 30, color, 2) return img try: img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype(self.DEFAULT_FONT_PATH, font_size) draw.text(pos, text, font=font, fill=tuple(reversed(color))) return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) except Exception as e: print(f"绘制中文失败: {e}") # 回退到默认字体 cv2.putText(img, text, pos, cv2.FONT_HERSHEY_SIMPLEX, font_size / 30, color, 2) return img def main(): """主函数""" try: # 模型选择 model_options = { 's': 'yolov8s-pose.pt', # 小模型,性能优先 'm': 'yolov8m-pose.pt', # 中等模型,平衡速度和精度 'l': 'yolov8l-pose.pt' # 大模型,精度优先 } print("请选择模型:") print("1. 小模型 (yolov8s-pose, 快速但精度较低)") print("2. 中模型 (yolov8m-pose, 平衡速度和精度)") print("3. 大模型 (yolov8l-pose, 高精度但较慢)") model_choice = input("请输入数字 (1-3, 默认2): ") model_key = { '1': 's', '2': 'm', '3': 'l' }.get(model_choice, 'm') model_path = model_options[model_key] print(f"使用模型: {model_path}") # 创建分析器实例,指定显示尺寸 display_width = 1280 display_height = 720 analyzer = DumbbellCurlAnalyzer(model_path, display_width, display_height) # 打开摄像头 print("正在打开摄像头...") cap = cv2.VideoCapture(0) # 设置摄像头分辨率 cap.set(cv2.CAP_PROP_FRAME_WIDTH, display_width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, display_height) # 检查摄像头是否成功打开 if not cap.isOpened(): print("无法打开摄像头") return # 设置窗口属性 cv2.namedWindow("哑铃弯举分析", cv2.WINDOW_NORMAL) cv2.resizeWindow("哑铃弯举分析", display_width, display_height) # 全屏标志 is_fullscreen = False # 模型质量级别 (影响处理速度) quality_level = 1 # 1=高(完整处理), 2=中(跳帧处理), 3=低(低分辨率) # 主循环 print("开始分析,请进行哑铃弯举动作...") print("操作提示:") print(" ESC: 退出程序") print(" F: 切换全屏显示") print(" S: 保存当前画面截图") print(" Q: 切换处理质量级别") while True: # 读取一帧 ret, frame = cap.read() # 检查是否成功读取帧 if not ret: print("无法获取帧") break # 根据质量级别调整处理方式 if quality_level == 3: # 低质量: 降低分辨率 frame = cv2.resize(frame, (640, 360)) elif quality_level == 2: # 中等质量: 增加跳帧间隔 analyzer.skip_interval = 3 else: # 高质量: 正常处理 analyzer.skip_interval = 2 # 分析帧 analysis_results, viz_frame = analyzer.analyze_frame(frame) # 显示结果 cv2.imshow("哑铃弯举分析", viz_frame) # 处理按键事件 key = cv2.waitKey(1) # ESC键退出 if key == 27: break # F键切换全屏 if key == ord('f') or key == ord('F'): is_fullscreen = not is_fullscreen if is_fullscreen: cv2.setWindowProperty("哑铃弯举分析", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) else: cv2.setWindowProperty("哑铃弯举分析", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_NORMAL) # S键保存当前帧 if key == ord('s') or key == ord('S'): timestamp = time.strftime("%Y%m%d-%H%M%S") filename = f"dumbbell_curl_{timestamp}.png" cv2.imwrite(filename, viz_frame) print(f"已保存截图: {filename}") # Q键切换质量级别 if key == ord('q') or key == ord('Q'): quality_level = (quality_level % 3) + 1 quality_names = ["高", "中", "低"] print(f"已切换处理质量: {quality_names[quality_level - 1]}") # 释放资源 cap.release() cv2.destroyAllWindows() print("程序已退出") except Exception as e: print(f"程序运行出错: {str(e)}") if __name__ == "__main__": main()
07-28
V27960021@dg03podv27960021kj4p:~/存储/casio/24608/存储2$ python storage.py ./测试 ./对比 24608_report_dual.xlsx 成功创建子目录对比页: system 成功创建子目录对比页: system_apex 成功创建子目录对比页: system_app 成功创建子目录对比页: system_xiangxi 成功创建子目录对比页: system_priv-app 成功创建子目录对比页: system_lib 成功创建子目录对比页: system_lib64 成功创建子目录对比页: system_etc 成功创建子目录对比页: system_framework 成功创建子目录对比页: system_fonts 成功创建子目录对比页: system_product 成功创建子目录对比页: system_ext 成功创建子目录对比页: system_ext_app 成功创建子目录对比页: system_ext_xiangxi 成功创建子目录对比页: system_ext_lib 成功创建子目录对比页: system_ext_lib64 成功创建子目录对比页: system_ext_etc 成功创建子目录对比页: system_ext_media 成功创建子目录对比页: vendor 成功创建子目录对比页: vendor_app 成功创建子目录对比页: vendor_xiangxi 成功创建子目录对比页: vendor_lib 成功创建子目录对比页: vendor_lib64 成功创建子目录对比页: vendor_etc 成功创建子目录对比页: vendor_bin Traceback (most recent call last): File "storage.py", line 215, in <module> generate_all_sheets(args.output, args.folder1, args.folder2) File "storage.py", line 195, in generate_all_sheets generate_directory_detail_sheet(wb, folder1, folder2, prefix, partition, suffix) File "storage.py", line 111, in generate_directory_detail_sheet data1 = parse_du_file(file1_path) File "storage.py", line 67, in parse_du_file size = float(size_str) / 1024.0 ValueError: could not convert string to float: du:
11-07
import tkinter as tk from tkinter import ttk, filedialog, messagebox import pandas as pd import threading import subprocess import datetime import queue import os import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties from collections import defaultdict import time import openpyxl from openpyxl.styles import PatternFill import matplotlib import platform import psutil import re matplotlib.use('Agg') # 避免与Tkinter冲突 class NetworkMonitorApp: def __init__(self, root): self.root = root self.root.title("高性能网络设备监控系统") self.root.geometry("1200x800") # 排序状态 self.sort_column = "" # 当前排序的列 self.sort_direction = "asc" # 排序方向: asc/desc # 创建状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 默认配置 self.config = { 'thread_count': 50, 'ping_interval': 5, 'ping_timeout': 1, 'excel_path': r"D:\1.xlsx", 'key_devices': ["二层交换机", "三层交换机", "UPS"], 'tvm_count': 3, 'agm_count': 4 } # 数据存储 self.device_data = {} self.disconnect_count = defaultdict(int) self.last_ping_results = {} self.current_status = {} self.sheet_data = {} self.pending_updates = set() # 需要更新的设备IP集合 self.last_update_time = time.time() # 上次GUI更新时间 self.update_threshold = 0.5 # GUI更新间隔(秒) # GUI组件初始化 self.create_widgets() self.load_excel_data() # 启动GUI更新线程 self.gui_update_queue = queue.Queue() self.root.after(100, self.process_gui_queue) # 设置中文字体 try: self.chinese_font = FontProperties(fname=r"C:\Windows\Fonts\simhei.ttf") except: self.chinese_font = FontProperties() print("警告:未找到中文字体文件,中文显示可能不正常") def create_widgets(self): # 控制面板 control_frame = ttk.LabelFrame(self.root, text="控制面板") control_frame.pack(fill=tk.X, padx=10, pady=5) self.start_btn = ttk.Button(control_frame, text="开始监控", command=self.start_monitoring) self.start_btn.pack(side=tk.LEFT, padx=5, pady=5) ttk.Button(control_frame, text="清除断网次数", command=self.reset_counters).pack(side=tk.LEFT, padx=5, pady=5) ttk.Button(control_frame, text="设置", command=self.open_settings).pack(side=tk.LEFT, padx=5, pady=5) export_frame = ttk.Frame(control_frame) export_frame.pack(side=tk.LEFT, padx=10) ttk.Label(export_frame, text="导出格式:").pack(side=tk.LEFT) self.export_format = tk.StringVar(value="Excel") ttk.Combobox(export_frame, textvariable=self.export_format, values=["Excel", "TXT", "图片"], width=8).pack(side=tk.LEFT, padx=2) ttk.Button(export_frame, text="导出", command=self.export_data).pack(side=tk.LEFT) # 设备表格框架 frame = ttk.Frame(self.root) frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) self.columns = ("序号", "时间戳", "线路", "设备IP", "车站", "设备名称", "断开次数", "状态") # 创建表格容器 container = ttk.Frame(frame) container.pack(fill=tk.BOTH, expand=True) # 创建Treeview self.tree = ttk.Treeview( container, columns=self.columns, show="headings", height=20 ) # 设置列宽 col_widths = [40, 150, 100, 120, 100, 150, 80, 100] for col, width in zip(self.columns, col_widths): self.tree.heading(col, text=col, command=lambda c=col: self.sort_treeview(c)) self.tree.column(col, width=width, anchor=tk.CENTER) # 添加垂直滚动条 vsb = ttk.Scrollbar(container, orient="vertical", command=self.tree.yview) self.tree.configure(yscrollcommand=vsb.set) # 添加水平滚动条 hsb = ttk.Scrollbar(container, orient="horizontal", command=self.tree.xview) self.tree.configure(xscrollcommand=hsb.set) # 布局 self.tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") # 配置网格行列权重 container.grid_rowconfigure(0, weight=1) container.grid_columnconfigure(0, weight=1) # 设置标签颜色 self.tree.tag_configure('disconnected', background='#ffcccc') self.tree.tag_configure('key_disconnected', background='#ffffcc') self.tree.tag_configure('critical_disconnected', background='#cce5ff') # 性能监控标签 perf_frame = ttk.LabelFrame(self.root, text="性能监控") perf_frame.pack(fill=tk.X, padx=10, pady=5) self.device_count_var = tk.StringVar(value="设备总数: 0") ttk.Label(perf_frame, textvariable=self.device_count_var).pack(side=tk.LEFT, padx=10) self.memory_usage_var = tk.StringVar(value="内存使用: 0 MB") ttk.Label(perf_frame, textvariable=self.memory_usage_var).pack(side=tk.LEFT, padx=10) self.ping_time_var = tk.StringVar(value="平均Ping时间: 0 ms") ttk.Label(perf_frame, textvariable=self.ping_time_var).pack(side=tk.LEFT, padx=10) # 启动性能监控 self.root.after(1000, self.update_performance_stats) def update_performance_stats(self): """更新性能统计信息""" # 计算内存使用 process = psutil.Process(os.getpid()) mem_usage = process.memory_info().rss / (1024 * 1024) self.memory_usage_var.set(f"内存使用: {mem_usage:.1f} MB") # 更新设备计数 device_count = len(self.device_data) self.device_count_var.set(f"设备总数: {device_count}") # 计算平均Ping时间 total_time = 0 count = 0 for ip, data in self.last_ping_results.items(): if data['success']: total_time += data['response_time'] count += 1 avg_time = total_time / count if count > 0 else 0 self.ping_time_var.set(f"平均Ping时间: {avg_time:.1f} ms") # 继续监控 self.root.after(5000, self.update_performance_stats) def sort_treeview(self, column): """点击列标题进行排序""" # 获取当前所有项目 items = [(self.tree.item(item, 'values'), item) for item in self.tree.get_children('')] if not items: return # 确定排序方向 if self.sort_column == column: self.sort_direction = "desc" if self.sort_direction == "asc" else "asc" else: self.sort_column = column self.sort_direction = "asc" # 获取列的索引 col_index = self.columns.index(column) # 定义排序函数 def sort_key(item): value = item[0][col_index] # 尝试转换为数字 try: return float(value) if value else 0 except ValueError: # 处理状态列的特殊情况 if column == "状态" and value == "断开": return -1 # 断开状态排在前面 return value.lower() # 字符串转换为小写比较,不区分大小写 # 排序 reverse = (self.sort_direction == "desc") items.sort(key=sort_key, reverse=reverse) # 更新表格 for index, (values, item_id) in enumerate(items): # 更新序号 new_values = list(values) new_values[0] = index + 1 # 更新行 self.tree.item(item_id, values=tuple(new_values)) # 每100条更新一次界面,避免卡顿 if index % 100 == 0: self.root.update_idletasks() # 更新列标题箭头指示 self.update_column_headers() self.status_var.set(f"已按 [{column}] 进行{'降序' if reverse else '升序'}排序") def update_column_headers(self): """更新列标题,显示排序指示器""" for col in self.columns: current_text = self.tree.heading(col, "text") # 移除现有的排序指示器 if current_text.endswith(" ↑") or current_text.endswith(" ↓"): current_text = current_text[:-2] # 添加新的排序指示器 if col == self.sort_column: direction = "↑" if self.sort_direction == "asc" else "↓" current_text += f" {direction}" self.tree.heading(col, text=current_text) def load_excel_data(self): try: # 清空现有数据 self.device_data.clear() self.sheet_data.clear() self.disconnect_count.clear() self.last_ping_results.clear() # 读取Excel所有sheet if not os.path.exists(self.config['excel_path']): messagebox.showerror("错误", f"Excel文件不存在: {self.config['excel_path']}") return # 使用openpyxl加速读取 wb = openpyxl.load_workbook(self.config['excel_path'], read_only=True) device_count = 0 for sheet_name in wb.sheetnames: sheet = wb[sheet_name] headers = [cell.value for cell in sheet[1]] # 确保列名存在 required_columns = ['设备IP', '车站', '设备名称'] if not all(col in headers for col in required_columns): missing = [col for col in required_columns if col not in headers] print(f"Sheet '{sheet_name}' 缺少必要列: {', '.join(missing)}") continue # 获取列索引 ip_col_idx = headers.index('设备IP') station_col_idx = headers.index('车站') device_name_col_idx = headers.index('设备名称') # 提取设备信息 for row in sheet.iter_rows(min_row=2): ip_cell = row[ip_col_idx] if not ip_cell.value: continue ip = str(ip_cell.value).strip() if not ip or ip.lower() == 'nan': continue # 存储设备信息 self.device_data[ip] = { 'sheet': sheet_name, 'station': row[station_col_idx].value if station_col_idx < len(row) else '', 'device_name': row[device_name_col_idx].value if device_name_col_idx < len(row) else '', 'ip': ip } device_count += 1 # 内存保护 - 检查当前内存使用 current_mem = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024) if current_mem > 500: # 500MB内存限制 messagebox.showwarning("内存警告", f"达到内存限制 (500MB),停止加载更多设备") break self.status_var.set(f"Sheet '{sheet_name}' 加载完成, 设备数: {device_count}") device_count = 0 # 重置计数器 self.status_var.set(f"Excel数据加载成功,共 {len(self.device_data)} 台设备") # 初始化ping结果 timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") for ip in self.device_data: self.last_ping_results[ip] = { 'timestamp': timestamp, 'success': True, 'response_time': 0, 'disconnect_count': 0 } # 初始化表格 self.initialize_treeview() except Exception as e: messagebox.showerror("错误", f"加载Excel失败: {str(e)}") finally: # 确保关闭工作簿 if 'wb' in locals(): wb.close() def initialize_treeview(self): """初始化表格视图(优化版)""" # 清空现有数据 for item in self.tree.get_children(): self.tree.delete(item) # 批量添加设备(每100个更新一次) devices = list(self.device_data.items()) batch_size = 100 for i in range(0, len(devices), batch_size): batch = devices[i:i + batch_size] for idx, (ip, device) in enumerate(batch, start=i + 1): ping_data = self.last_ping_results.get(ip, { 'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 'success': True, 'response_time': 0, 'disconnect_count': 0 }) # 确定标签 tags = () if not ping_data['success']: if device['device_name'] in self.config['key_devices']: tags = ('key_disconnected',) elif self.is_critical_device(ip): tags = ('critical_disconnected',) else: tags = ('disconnected',) # 添加行 self.tree.insert("", "end", values=( idx, ping_data['timestamp'], device['sheet'], ip, device['station'], device['device_name'], ping_data['disconnect_count'], f"{ping_data['response_time']}ms" if ping_data['success'] else "断开" ), tags=tags) # 更新GUI以显示进度 self.root.update_idletasks() # 初始化列标题排序指示器 self.update_column_headers() def start_monitoring(self): if not self.device_data: messagebox.showwarning("警告", "没有可监控的设备,请检查Excel文件路径和内容") return if self.start_btn.cget('text') == '开始监控': self.monitoring_active = True self.start_btn.config(text='停止监控') self.status_var.set("监控已启动...") # 启动监控线程 self.monitor_thread = threading.Thread(target=self.run_monitoring, daemon=True) self.monitor_thread.start() else: self.monitoring_active = False self.start_btn.config(text='开始监控') self.status_var.set("监控已停止") def run_monitoring(self): """优化后的监控循环,支持5000台设备""" from concurrent.futures import ThreadPoolExecutor, as_completed # 使用固定线程池 executor = ThreadPoolExecutor(max_workers=self.config['thread_count']) # 分批处理参数 batch_size = 200 # 每批处理200台设备 devices = list(self.device_data.keys()) # 性能统计 total_ping_time = 0 ping_count = 0 while self.monitoring_active: start_time = time.time() # 分批处理设备 for i in range(0, len(devices), batch_size): if not self.monitoring_active: break batch = devices[i:i + batch_size] futures = {executor.submit(self.ping_device, ip): ip for ip in batch} for future in as_completed(futures): ip = futures[future] try: success, response_time = future.result() except Exception as e: success, response_time = False, 0 # 更新性能统计 if success and response_time > 0: total_ping_time += response_time ping_count += 1 # 更新状态 timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") device_info = self.device_data[ip] # 只记录断开状态的设备 if not success: self.disconnect_count[ip] += 1 # 获取之前的状态 prev_data = self.last_ping_results.get(ip, {'success': True}) prev_status = prev_data['success'] prev_count = prev_data.get('disconnect_count', 0) # 仅当状态变化时才更新结果 if prev_status != success or prev_count != self.disconnect_count[ip]: self.last_ping_results[ip] = { 'timestamp': timestamp, 'success': success, 'response_time': response_time, 'disconnect_count': self.disconnect_count[ip] } # 标记需要更新GUI的设备 self.pending_updates.add(ip) # 智能GUI更新 current_time = time.time() if current_time - self.last_update_time > self.update_threshold: self.update_gui() self.last_update_time = current_time self.pending_updates.clear() # 计算剩余等待时间 elapsed = time.time() - start_time sleep_time = max(0, self.config['ping_interval'] - elapsed) # 更新状态栏显示进度 avg_ping = total_ping_time / ping_count if ping_count > 0 else 0 self.status_var.set( f"监控中... 设备数: {len(devices)} | " f"本轮耗时: {elapsed:.1f}s | " f"平均Ping: {avg_ping:.1f}ms | " f"下一轮: {sleep_time:.1f}s" ) time.sleep(sleep_time) def ping_device(self, ip): """高性能Ping实现,针对大规模网络优化""" try: # 根据操作系统选择不同的Ping命令 if platform.system().lower() == "windows": # Windows使用无窗口模式ping命令 timeout_ms = int(self.config['ping_timeout'] * 1000) command = ['ping', '-n', '1', '-w', str(timeout_ms), ip] creation_flags = subprocess.CREATE_NO_WINDOW else: # Linux/Mac command = ['ping', '-c', '1', '-W', str(self.config['ping_timeout']), ip] creation_flags = 0 # 使用subprocess.Popen实现非阻塞Ping with subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=creation_flags, text=True, bufsize=1 ) as process: # 设置超时 try: # 等待进程完成,但不超过超时时间 process.wait(timeout=self.config['ping_timeout']) except subprocess.TimeoutExpired: process.kill() return False, 0 # 检查进程返回码 if process.returncode == 0: # 快速检查输出中是否包含TTL/ttl字段 output = process.stdout.read() if "TTL=" in output or "ttl=" in output.lower(): # 提取响应时间 time_lines = [line for line in output.split('\n') if '时间=' in line or 'time=' in line] if time_lines: time_str = time_lines[0] match = re.search(r'(\d+)\s?ms', time_str) if match: response_time = int(match.group(1)) return True, response_time return True, 1 # 成功返回1ms(不计算具体时间) return False, 0 except: return False, 0 def update_gui(self): """只更新需要刷新的设备行""" if not self.pending_updates: return # 获取当前显示的项目 current_items = self.tree.get_children() update_count = 0 # 只更新需要更新的设备 for ip in list(self.pending_updates): if ip not in self.device_data: continue device = self.device_data[ip] ping_data = self.last_ping_results.get(ip, {}) if not ping_data: continue # 找到并更新对应的行 for item in current_items: item_values = self.tree.item(item, 'values') if item_values and len(item_values) > 3 and item_values[3] == ip: # IP地址在第4列 # 更新值 new_values = ( item_values[0], # 保持序号不变 ping_data['timestamp'], device['sheet'], ip, device['station'], device['device_name'], ping_data['disconnect_count'], f"{ping_data['response_time']}ms" if ping_data.get('success', False) else "断开" ) self.tree.item(item, values=new_values) # 根据状态设置颜色 if not ping_data.get('success', False): if device['device_name'] in self.config['key_devices']: self.tree.item(item, tags=('key_disconnected',)) elif self.is_critical_device(ip): self.tree.item(item, tags=('critical_disconnected',)) else: self.tree.item(item, tags=('disconnected',)) else: self.tree.item(item, tags=()) update_count += 1 break # 更新状态栏 self.status_var.set(f"更新了 {update_count} 台设备状态") self.pending_updates.clear() def is_critical_device(self, ip): device_info = self.device_data[ip] sheet = device_info['sheet'] station = device_info['station'] # 计算同站同类设备断网数量 same_station_devices = [ d for d in self.device_data.values() if d['sheet'] == sheet and d['station'] == station ] # 检查TVM if "TVM" in device_info['device_name']: disconnected_tvm = sum( 1 for d in same_station_devices if "TVM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_tvm >= self.config['tvm_count'] # 检查AGM elif "AGM" in device_info['device_name']: disconnected_agm = sum( 1 for d in same_station_devices if "AGM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_agm >= self.config['agm_count'] return False def reset_counters(self): self.disconnect_count.clear() for ip in self.last_ping_results: self.last_ping_results[ip]['disconnect_count'] = 0 self.update_gui() self.status_var.set("断网次数已清零") def open_settings(self): settings_win = tk.Toplevel(self.root) settings_win.title("系统设置") settings_win.geometry("450x400") # 创建设置控件 entries = {} row = 0 # 配置项中文标签映射 config_labels = { 'thread_count': '并发线程数:', 'ping_interval': 'Ping间隔(秒):', 'ping_timeout': 'Ping超时(秒):', 'excel_path': 'Excel文件路径:', 'key_devices': '关键设备类型:', 'tvm_count': '同站TVM断网阈值:', 'agm_count': '同站AGM断网阈值:' } for key, label in config_labels.items(): ttk.Label(settings_win, text=label).grid(row=row, column=0, padx=10, pady=5, sticky="e") # 特殊处理关键设备类型(逗号分隔) if key == 'key_devices': entry = ttk.Entry(settings_win, width=30) entry.insert(0, ",".join(self.config[key])) else: entry = ttk.Entry(settings_win, width=30) entry.insert(0, str(self.config[key])) entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") entries[key] = entry # 仅为Excel路径添加浏览按钮 if key == 'excel_path': def browse_excel(entry_widget=entry): # 使用闭包绑定正确的entry file_path = filedialog.askopenfilename( filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")] ) if file_path: entry_widget.delete(0, tk.END) entry_widget.insert(0, file_path) browse_btn = ttk.Button(settings_win, text="浏览", command=browse_excel) browse_btn.grid(row=row, column=2, padx=5, pady=5) row += 1 def save_settings(): try: for key, entry in entries.items(): value = entry.get() if key == 'thread_count': self.config[key] = int(value) elif key in ['ping_interval', 'ping_timeout']: self.config[key] = float(value) elif key == 'key_devices': self.config[key] = [d.strip() for d in value.split(",")] elif key in ['tvm_count', 'agm_count']: self.config[key] = int(value) else: self.config[key] = value # 重新加载Excel self.load_excel_data() settings_win.destroy() self.status_var.set("设置已保存并应用") except Exception as e: messagebox.showerror("错误", f"保存设置失败: {str(e)}") # 保存按钮 btn_frame = ttk.Frame(settings_win) btn_frame.grid(row=row, column=0, columnspan=3, pady=10) ttk.Button(btn_frame, text="保存", command=save_settings).pack(side=tk.LEFT, padx=10) ttk.Button(btn_frame, text="取消", command=settings_win.destroy).pack(side=tk.LEFT, padx=10) def export_data(self): export_type = self.export_format.get() # 获取断网设备数据 disconnected_devices = [] for ip, ping_data in self.last_ping_results.items(): if not ping_data['success']: device = self.device_data[ip] disconnected_devices.append({ **device, **ping_data, 'ip': ip }) if not disconnected_devices: messagebox.showinfo("导出", "没有断网设备可导出") return # 创建DataFrame df = pd.DataFrame(disconnected_devices) df = df[['timestamp', 'sheet', 'ip', 'station', 'device_name', 'disconnect_count']] df.columns = ['时间戳', '线路', '设备IP', '车站', '设备名称', '断开次数'] # 排序 df['类型'] = df.apply(lambda row: '关键设备' if row['设备名称'] in self.config['key_devices'] else '重要设备' if self.is_critical_device(row['设备IP']) else '普通设备', axis=1) df = df.sort_values(by=['类型', '断开次数'], ascending=[True, False]) try: # 设置默认文件名 default_filename = f"断网设备报告_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}" if export_type == "Excel": file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", initialfile=default_filename, filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")] ) if not file_path: return # 添加样式 writer = pd.ExcelWriter(file_path, engine='openpyxl') df.to_excel(writer, index=False, sheet_name='断网设备') # 获取工作簿和工作表 workbook = writer.book worksheet = writer.sheets['断网设备'] # 添加颜色样式 yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") lightblue_fill = PatternFill(start_color="00FFFF", end_color="00FFFF", fill_type="solid") red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid") # 设置标题行样式 for col in range(1, len(df.columns) + 1): cell = worksheet.cell(row=1, column=col) cell.fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid") # 设置数据行样式 for idx, row in enumerate(df.itertuples(), start=2): device_name = row[5] # 设备名称在第五列 ip = row[3] # 设备IP在第三列 if device_name in self.config['key_devices']: fill = yellow_fill elif self.is_critical_device(ip): fill = lightblue_fill else: fill = red_fill # 设置整行颜色 for col in range(1, len(df.columns) + 1): cell = worksheet.cell(row=idx, column=col) cell.fill = fill # 调整列宽 for col in worksheet.columns: max_length = 0 column = col[0].column_letter for cell in col: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = (max_length + 2) worksheet.column_dimensions[column].width = adjusted_width writer.close() elif export_type == "TXT": file_path = filedialog.asksaveasfilename( defaultextension=".txt", initialfile=default_filename, filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] ) if not file_path: return with open(file_path, 'w', encoding='utf-8') as f: f.write("断网设备报告\n") f.write(f"生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"总断网设备数: {len(df)}\n") f.write("=" * 80 + "\n") for _, row in df.iterrows(): f.write(f"线路: {row['线路']}\n") f.write(f"设备IP: {row['设备IP']}\n") f.write(f"车站: {row['车站']}\n") f.write(f"设备名称: {row['设备名称']}\n") f.write(f"断开次数: {row['断开次数']}\n") f.write(f"最后检测时间: {row['时间戳']}\n") f.write(f"设备类型: {row['类型']}\n") f.write("-" * 80 + "\n") elif export_type == "图片": file_path = filedialog.asksaveasfilename( defaultextension=".png", initialfile=default_filename, filetypes=[("PNG图片", "*.png"), ("JPEG图片", "*.jpg"), ("所有文件", "*.*")] ) if not file_path: return # 设置中文显示 plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体 plt.rcParams['axes.unicode_minus'] = False # 正常显示负号 # 创建图表 plt.figure(figsize=(15, 8)) plt.title(f"断网设备统计 ({datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')})", fontsize=14) # 按线路分组统计 line_counts = df.groupby('线路').size() line_counts.plot(kind='bar', color='skyblue') plt.xlabel('线路') plt.ylabel('断网设备数量') plt.xticks(rotation=45) plt.tight_layout() # 保存图片 plt.savefig(file_path, dpi=150) plt.close() self.status_var.set(f"导出成功: {os.path.basename(file_path)}") except Exception as e: messagebox.showerror("导出错误", f"导出失败: {str(e)}") def process_gui_queue(self): """处理GUI更新队列""" try: while True: task = self.gui_update_queue.get_nowait() task() except queue.Empty: pass finally: self.root.after(100, self.process_gui_queue) def is_critical_device(self, ip): device_info = self.device_data[ip] sheet = device_info['sheet'] station = device_info['station'] # 计算同站同类设备断网数量 same_station_devices = [ d for d in self.device_data.values() if d['sheet'] == sheet and d['station'] == station ] # 检查TVM if "TVM" in device_info['device_name']: disconnected_tvm = sum( 1 for d in same_station_devices if "TVM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_tvm >= self.config['tvm_count'] # 检查AGM elif "AGM" in device_info['device_name']: disconnected_agm = sum( 1 for d in same_station_devices if "AGM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_agm >= self.config['agm_count'] return False if __name__ == "__main__": root = tk.Tk() app = NetworkMonitorApp(root) root.mainloop() 窗口优化提升刷新速度,不卡顿,且高效
最新发布
11-28
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值