python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python统计C++代码行数

Python+PyQt5设计实现一个C++源代码行数统计工具

作者:老歌老听老掉牙

在软件开发过程中,代码行数是一个基础但重要的软件度量指标,本文介绍一个基于PyQt5的图形化C++源代码行数统计工具,感兴趣的小伙伴可以了解下

引言

在软件开发过程中,代码行数(Lines of Code, LOC)是一个基础但重要的软件度量指标。虽然它不能完全代表代码质量或开发效率,但在项目规模评估、进度跟踪和复杂度分析中仍然具有参考价值。对于C++项目而言,由于头文件(.h)和实现文件(.cpp)的分离,需要一个专门的工具来准确统计源代码规模。

本文介绍一个基于PyQt5的图形化C++源代码行数统计工具,该工具能够递归遍历指定目录,统计所有C++源文件的行数,并提供详细的分类统计结果。

程序设计架构

整体架构设计

该工具采用经典的Model-View-Controller(MVC)架构模式,但在PyQt5的上下文中有所调整:

多线程设计

考虑到大型项目可能包含数千个源文件,统计过程可能耗时较长,因此采用多线程设计至关重要。主线程负责UI渲染和事件处理,工作线程负责文件统计,通过PyQt5的信号-槽机制实现线程间通信。

其中Ttotal是总耗时, Ttraverse是文件遍历时间, Tread是文件读取时间,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 QFont

class CodeLineCounter(QMainWindow):
    def __init__(self):
        super().__init__()
        self.init_ui()
        self.selected_directory = ""
        self.count_thread = None
        
    def 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 = directory
        self.file_extensions = ['.h', '.cpp', '.hpp', '.cc', '.cxx', '.c', '.hh']
        
    def run(self):
        file_results = {}
        total_lines = 0
        file_count = 0
        
        cpp_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_count
                total_lines += line_count
                self.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_files
    
    def 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 = directory
            self.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, '警告', '请先选择目录!')
            return
            
        self.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) + 1
        return file_types
    
    def 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 text
    
    def generate_file_type_statistics(self, file_types):
        text = "文件类型统计:\n"
        for ext, count in sorted(file_types.items()):
            text += f"  {ext}: {count} 个文件\n"
        return text
    
    def 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),其中 n是目录中的文件和子目录总数。

其中d是目录树的最大深度,bi是第 i 层的分支因子。

行数统计算法

行数统计采用简单的逐行读取方法,对于每个文件:

这种方法的时间复杂度为O(m),其中 m 是文件中的行数。

内存优化策略

对于极大的源文件,我们采用流式读取而非一次性加载到内存:

def count_file_lines_streaming(file_path):
    try:
        line_count = 0
        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
            for line in f:
                line_count += 1
        return line_count
    except 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 QFont

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 = directory
        self.file_extensions = ['.h', '.cpp', '.hpp', '.cc', '.cxx', '.c', '.hh']
        
    def run(self):
        file_results = {}
        total_lines = 0
        
        cpp_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_count
                total_lines += line_count
                self.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_files
    
    def count_file_lines(self, file_path):
        try:
            line_count = 0
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                for line in f:
                    line_count += 1
            return line_count
        except Exception as e:
            print(f"无法读取文件 {file_path}: {e}")
            return -1

class CodeLineCounter(QMainWindow):
    def __init__(self):
        super().__init__()
        self.init_ui()
        self.selected_directory = ""
        self.count_thread = None
        
    def 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 = directory
            self.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, '警告', '请先选择目录!')
            return
            
        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('正在统计中...')
        
        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) + 1
        
        detail_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

主要功能特点

性能分析与优化建议

性能瓶颈分析

在实际使用中,主要性能瓶颈可能出现在:

优化策略

扩展功能建议

结论

本文介绍的C++源代码行数统计工具基于PyQt5框架,采用了多线程架构和模块化设计,具有良好的用户体验和可扩展性。通过数学建模和算法分析,我们深入探讨了工具的性能特征和优化方向。

该工具不仅满足了基本的代码行数统计需求,还为后续的功能扩展奠定了坚实基础。在实际软件开发过程中,这样的工具能够帮助开发团队更好地理解项目规模,进行有效的项目管理和技术决策。

代码行数虽然只是一个基础度量指标,但在恰当的语境下,结合其他质量指标,仍然能够为软件工程实践提供有价值的参考信息。

到此这篇关于Python+PyQt5设计实现一个C++源代码行数统计工具的文章就介绍到这了,更多相关Python统计C++代码行数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文