C++源代码行数统计工具的设计与实现
引言
在软件开发过程中,代码行数(Lines of Code, LOC)是一个基础但重要的软件度量指标。虽然它不能完全代表代码质量或开发效率,但在项目规模评估、进度跟踪和复杂度分析中仍然具有参考价值。对于C++项目而言,由于头文件(.h)和实现文件(.cpp)的分离,需要一个专门的工具来准确统计源代码规模。
本文介绍一个基于PyQt5的图形化C++源代码行数统计工具,该工具能够递归遍历指定目录,统计所有C++源文件的行数,并提供详细的分类统计结果。
程序设计架构
整体架构设计
该工具采用经典的Model-View-Controller(MVC)架构模式,但在PyQt5的上下文中有所调整:
- View层:由PyQt5的各类窗口部件组成,负责用户交互和结果展示
- Controller层:处理用户事件,协调View和Model之间的数据流
- Model层:文件统计线程,负责实际的文件遍历和行数统计
多线程设计
考虑到大型项目可能包含数千个源文件,统计过程可能耗时较长,因此采用多线程设计至关重要。主线程负责UI渲染和事件处理,工作线程负责文件统计,通过PyQt5的信号-槽机制实现线程间通信。
Ttotal=∑i=1n(Ttraversei+Treadi+Tcounti)T_{total} = \sum_{i=1}^{n} (T_{traverse_i} + T_{read_i} + T_{count_i})Ttotal=i=1∑n(Ttraversei+Treadi+Tcounti)
其中TtotalT_{total}Ttotal是总耗时,TtraverseT_{traverse}Ttraverse是文件遍历时间,TreadT_{read}Tread是文件读取时间,TcountT_{count}Tcount是行数统计时间。
核心模块实现
主窗口类设计
import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTextEdit, QFileDialog, QWidget, QProgressBar, QMessageBox, QSplitter)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFontclass CodeLineCounter(QMainWindow):def __init__(self):super().__init__()self.init_ui()self.selected_directory = ""self.count_thread = Nonedef init_ui(self):self.setWindowTitle('C++源代码行数统计工具')self.setGeometry(100, 100, 900, 700)central_widget = QWidget()self.setCentralWidget(central_widget)main_layout = QVBoxLayout(central_widget)# 创建界面组件self.create_control_panel(main_layout)self.create_progress_bar(main_layout)self.create_result_display(main_layout)def create_control_panel(self, parent_layout):control_layout = QHBoxLayout()self.select_btn = QPushButton('选择目录')self.select_btn.clicked.connect(self.select_directory)self.count_btn = QPushButton('开始统计')self.count_btn.clicked.connect(self.start_counting)self.count_btn.setEnabled(False)self.path_label = QLabel('未选择目录')self.path_label.setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; }")control_layout.addWidget(self.select_btn)control_layout.addWidget(self.count_btn)control_layout.addWidget(self.path_label, 1)parent_layout.addLayout(control_layout)def create_progress_bar(self, parent_layout):self.progress_bar = QProgressBar()self.progress_bar.setVisible(False)parent_layout.addWidget(self.progress_bar)def create_result_display(self, parent_layout):self.result_label = QLabel('请选择包含C++源代码的目录')self.result_label.setAlignment(Qt.AlignCenter)self.result_label.setStyleSheet("QLabel { font-size: 14px; padding: 10px; }")parent_layout.addWidget(self.result_label)splitter = QSplitter(Qt.Vertical)self.file_list = QTextEdit()self.file_list.setReadOnly(True)self.file_list.setPlaceholderText('文件统计结果将显示在这里...')self.file_list.setFont(QFont("Consolas", 9))self.detail_text = QTextEdit()self.detail_text.setReadOnly(True)self.detail_text.setPlaceholderText('详细统计信息将显示在这里...')self.detail_text.setFont(QFont("Consolas", 9))splitter.addWidget(self.file_list)splitter.addWidget(self.detail_text)splitter.setSizes([400, 200])parent_layout.addWidget(splitter, 1)
文件统计线程类
class FileCounterThread(QThread):progress_updated = pyqtSignal(int)file_counted = pyqtSignal(str, int)finished_counting = pyqtSignal(dict, int)def __init__(self, directory):super().__init__()self.directory = directoryself.file_extensions = ['.h', '.cpp', '.hpp', '.cc', '.cxx', '.c', '.hh']def run(self):file_results = {}total_lines = 0file_count = 0cpp_files = self.find_cpp_files()total_files = len(cpp_files)for i, file_path in enumerate(cpp_files):line_count = self.count_file_lines(file_path)if line_count >= 0:file_results[file_path] = line_counttotal_lines += line_countself.file_counted.emit(file_path, line_count)progress = int((i + 1) / total_files * 100)self.progress_updated.emit(progress)self.finished_counting.emit(file_results, total_lines)def find_cpp_files(self):cpp_files = []for root, dirs, files in os.walk(self.directory):for file in files:if any(file.lower().endswith(ext) for ext in self.file_extensions):cpp_files.append(os.path.join(root, file))return cpp_filesdef count_file_lines(self, file_path):try:with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:lines = f.readlines()return len(lines)except Exception as e:print(f"无法读取文件 {file_path}: {e}")return -1
事件处理逻辑
class CodeLineCounter(QMainWindow):# ... 前面的代码 ...def select_directory(self):directory = QFileDialog.getExistingDirectory(self, '选择包含C++源代码的目录','',QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)if directory:self.selected_directory = directoryself.path_label.setText(directory)self.count_btn.setEnabled(True)self.result_label.setText('目录已选择,点击"开始统计"按钮进行统计')def start_counting(self):if not self.selected_directory:QMessageBox.warning(self, '警告', '请先选择目录!')returnself.reset_ui_for_counting()self.start_counting_thread()def reset_ui_for_counting(self):self.file_list.clear()self.detail_text.clear()self.progress_bar.setVisible(True)self.progress_bar.setValue(0)self.count_btn.setEnabled(False)self.select_btn.setEnabled(False)self.result_label.setText('正在统计中...')def start_counting_thread(self):self.count_thread = FileCounterThread(self.selected_directory)self.count_thread.file_counted.connect(self.on_file_counted)self.count_thread.progress_updated.connect(self.progress_bar.setValue)self.count_thread.finished_counting.connect(self.on_counting_finished)self.count_thread.start()def on_file_counted(self, file_path, line_count):rel_path = os.path.relpath(file_path, self.selected_directory)self.file_list.append(f"{rel_path}: {line_count} 行")def on_counting_finished(self, file_results, total_lines):self.progress_bar.setVisible(False)self.count_btn.setEnabled(True)self.select_btn.setEnabled(True)self.display_final_results(file_results, total_lines)def display_final_results(self, file_results, total_lines):file_types = self.analyze_file_types(file_results)detail_text = self.generate_summary_text(file_results, total_lines, file_types)detail_text += self.generate_file_type_statistics(file_types)detail_text += self.generate_top_files_list(file_results)self.detail_text.setText(detail_text)self.result_label.setText(f'统计完成!共 {len(file_results)} 个文件,总行数: {total_lines:,}')def analyze_file_types(self, file_results):file_types = {}for file_path in file_results.keys():ext = os.path.splitext(file_path)[1].lower()file_types[ext] = file_types.get(ext, 0) + 1return file_typesdef generate_summary_text(self, file_results, total_lines, file_types):text = f"统计完成!\n"text += f"=" * 50 + "\n"text += f"目录: {self.selected_directory}\n"text += f"总文件数: {len(file_results)}\n"text += f"总行数: {total_lines:,}\n\n"return textdef generate_file_type_statistics(self, file_types):text = "文件类型统计:\n"for ext, count in sorted(file_types.items()):text += f" {ext}: {count} 个文件\n"return textdef generate_top_files_list(self, file_results):if not file_results:return ""sorted_files = sorted(file_results.items(), key=lambda x: x[1], reverse=True)text = f"\n行数最多的文件 (前10个):\n"for i, (file_path, line_count) in enumerate(sorted_files[:10]):rel_path = os.path.relpath(file_path, self.selected_directory)text += f" {i+1}. {rel_path}: {line_count:,} 行\n"return text
算法分析与优化
文件遍历算法
文件遍历采用深度优先搜索(DFS)策略,使用os.walk()
函数递归遍历目录树。该算法的时间复杂度为O(n)O(n)O(n),其中nnn是目录中的文件和子目录总数。
Ttraversal=O(∑i=1dbi)T_{traversal} = O(\sum_{i=1}^{d} b_i)Ttraversal=O(i=1∑dbi)
其中ddd是目录树的最大深度,bib_ibi是第iii层的分支因子。
行数统计算法
行数统计采用简单的逐行读取方法,对于每个文件:
- 打开文件并设置适当的编码处理
- 读取所有行到内存中
- 统计行数
这种方法的时间复杂度为O(m)O(m)O(m),其中mmm是文件中的行数。
内存优化策略
对于极大的源文件,我们采用流式读取而非一次性加载到内存:
def count_file_lines_streaming(file_path):try:line_count = 0with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:for line in f:line_count += 1return line_countexcept Exception as e:print(f"无法读取文件 {file_path}: {e}")return -1
完整可运行代码
import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTextEdit, QFileDialog, QWidget, QProgressBar, QMessageBox, QSplitter)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFontclass FileCounterThread(QThread):progress_updated = pyqtSignal(int)file_counted = pyqtSignal(str, int)finished_counting = pyqtSignal(dict, int)def __init__(self, directory):super().__init__()self.directory = directoryself.file_extensions = ['.h', '.cpp', '.hpp', '.cc', '.cxx', '.c', '.hh']def run(self):file_results = {}total_lines = 0cpp_files = self.find_cpp_files()total_files = len(cpp_files)for i, file_path in enumerate(cpp_files):line_count = self.count_file_lines(file_path)if line_count >= 0:file_results[file_path] = line_counttotal_lines += line_countself.file_counted.emit(file_path, line_count)progress = int((i + 1) / total_files * 100)self.progress_updated.emit(progress)self.finished_counting.emit(file_results, total_lines)def find_cpp_files(self):cpp_files = []for root, dirs, files in os.walk(self.directory):for file in files:if any(file.lower().endswith(ext) for ext in self.file_extensions):cpp_files.append(os.path.join(root, file))return cpp_filesdef count_file_lines(self, file_path):try:line_count = 0with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:for line in f:line_count += 1return line_countexcept Exception as e:print(f"无法读取文件 {file_path}: {e}")return -1class CodeLineCounter(QMainWindow):def __init__(self):super().__init__()self.init_ui()self.selected_directory = ""self.count_thread = Nonedef init_ui(self):self.setWindowTitle('C++源代码行数统计工具')self.setGeometry(100, 100, 900, 700)central_widget = QWidget()self.setCentralWidget(central_widget)main_layout = QVBoxLayout(central_widget)control_layout = QHBoxLayout()self.select_btn = QPushButton('选择目录')self.select_btn.clicked.connect(self.select_directory)self.count_btn = QPushButton('开始统计')self.count_btn.clicked.connect(self.start_counting)self.count_btn.setEnabled(False)self.path_label = QLabel('未选择目录')self.path_label.setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; }")control_layout.addWidget(self.select_btn)control_layout.addWidget(self.count_btn)control_layout.addWidget(self.path_label, 1)self.progress_bar = QProgressBar()self.progress_bar.setVisible(False)self.result_label = QLabel('请选择包含C++源代码的目录')self.result_label.setAlignment(Qt.AlignCenter)self.result_label.setStyleSheet("QLabel { font-size: 14px; padding: 10px; }")splitter = QSplitter(Qt.Vertical)self.file_list = QTextEdit()self.file_list.setReadOnly(True)self.file_list.setPlaceholderText('文件统计结果将显示在这里...')self.file_list.setFont(QFont("Consolas", 9))self.detail_text = QTextEdit()self.detail_text.setReadOnly(True)self.detail_text.setPlaceholderText('详细统计信息将显示在这里...')self.detail_text.setFont(QFont("Consolas", 9))splitter.addWidget(self.file_list)splitter.addWidget(self.detail_text)splitter.setSizes([400, 200])main_layout.addLayout(control_layout)main_layout.addWidget(self.progress_bar)main_layout.addWidget(self.result_label)main_layout.addWidget(splitter, 1)def select_directory(self):directory = QFileDialog.getExistingDirectory(self, '选择包含C++源代码的目录','',QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)if directory:self.selected_directory = directoryself.path_label.setText(directory)self.count_btn.setEnabled(True)self.result_label.setText('目录已选择,点击"开始统计"按钮进行统计')def start_counting(self):if not self.selected_directory:QMessageBox.warning(self, '警告', '请先选择目录!')returnself.file_list.clear()self.detail_text.clear()self.progress_bar.setVisible(True)self.progress_bar.setValue(0)self.count_btn.setEnabled(False)self.select_btn.setEnabled(False)self.result_label.setText('正在统计中...')self.count_thread = FileCounterThread(self.selected_directory)self.count_thread.file_counted.connect(self.on_file_counted)self.count_thread.progress_updated.connect(self.progress_bar.setValue)self.count_thread.finished_counting.connect(self.on_counting_finished)self.count_thread.start()def on_file_counted(self, file_path, line_count):rel_path = os.path.relpath(file_path, self.selected_directory)self.file_list.append(f"{rel_path}: {line_count} 行")def on_counting_finished(self, file_results, total_lines):self.progress_bar.setVisible(False)self.count_btn.setEnabled(True)self.select_btn.setEnabled(True)file_types = {}for file_path, line_count in file_results.items():ext = os.path.splitext(file_path)[1].lower()file_types[ext] = file_types.get(ext, 0) + 1detail_text = f"统计完成!\n"detail_text += f"=" * 50 + "\n"detail_text += f"目录: {self.selected_directory}\n"detail_text += f"总文件数: {len(file_results)}\n"detail_text += f"总行数: {total_lines:,}\n\n"detail_text += "文件类型统计:\n"for ext, count in sorted(file_types.items()):detail_text += f" {ext}: {count} 个文件\n"if file_results:sorted_files = sorted(file_results.items(), key=lambda x: x[1], reverse=True)detail_text += f"\n行数最多的文件 (前10个):\n"for i, (file_path, line_count) in enumerate(sorted_files[:10]):rel_path = os.path.relpath(file_path, self.selected_directory)detail_text += f" {i+1}. {rel_path}: {line_count:,} 行\n"self.detail_text.setText(detail_text)self.result_label.setText(f'统计完成!共 {len(file_results)} 个文件,总行数: {total_lines:,}')def main():app = QApplication(sys.argv)app.setApplicationName('C++代码行数统计工具')window = CodeLineCounter()window.show()sys.exit(app.exec_())if __name__ == '__main__':main()
使用说明与功能特点
安装依赖
pip install PyQt5
运行程序
python cpp_line_counter.py
主要功能特点
- 多格式支持:支持.h, .cpp, .hpp, .cc, .cxx, .c, .hh等多种C++文件格式
- 递归统计:自动递归遍历所有子目录
- 实时显示:统计过程中实时显示每个文件的结果
- 进度反馈:进度条显示整体统计进度
- 详细报告:提供文件类型统计和最大文件排名
- 错误容错:自动处理无法读取的文件,继续统计其他文件
- 编码兼容:支持UTF-8编码,兼容各种编码格式的源文件
性能分析与优化建议
性能瓶颈分析
在实际使用中,主要性能瓶颈可能出现在:
- I/O操作:大量小文件的读取操作
- 内存使用:极大文件的处理
- UI更新:频繁的界面刷新
优化策略
- 批量处理:将小文件分组批量读取
- 异步I/O:使用异步文件读取提高并发性
- 采样统计:对于极大文件,可以采用采样统计方法
- 缓存机制:缓存已统计文件的结果
扩展功能建议
- 代码复杂度分析:集成圈复杂度、函数长度等指标
- 注释率统计:区分代码行和注释行
- 历史对比:支持不同版本间的代码变化统计
- 导出功能:支持将统计结果导出为CSV或Excel格式
结论
本文介绍的C++源代码行数统计工具基于PyQt5框架,采用了多线程架构和模块化设计,具有良好的用户体验和可扩展性。通过数学建模和算法分析,我们深入探讨了工具的性能特征和优化方向。
该工具不仅满足了基本的代码行数统计需求,还为后续的功能扩展奠定了坚实基础。在实际软件开发过程中,这样的工具能够帮助开发团队更好地理解项目规模,进行有效的项目管理和技术决策。
代码行数虽然只是一个基础度量指标,但在恰当的语境下,结合其他质量指标,仍然能够为软件工程实践提供有价值的参考信息。