JSON结构化数据可视化工具-python-PyQt5

目录

  • 1、UI界面展示
  • 2、源代码

UI界面展示

精简,高效可视化JSON内部数据及其数据类型,
而且不会一次性加载全部数据,而是加载主要数据

JSON结构化数据可视化工具-python-PyQt5_第1张图片

源码

python>=3.10
直接放一个.py文件里就可以运行

# -*- coding: utf-8 -*-
# view.py - JSON探索器应用程序

import sys
import os
import json
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QPushButton, QLabel, QFileDialog,
                             QLineEdit, QTreeWidget, QTreeWidgetItem, QScrollArea,
                             QFrame, QMessageBox, QHeaderView, QComboBox, QAbstractItemView)
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon, QFont


class JSONExplorer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.current_json_data = None
        self.page_size = 5  # 每页显示的最大数量 - 确保与展示的项目数一致

    def initUI(self):
        # 设置窗口基本属性
        self.setWindowTitle('JSON探索器')
        self.setGeometry(100, 100, 1000, 800)

        # 创建主窗口部件和布局
        central_widget = QWidget()
        main_layout = QVBoxLayout(central_widget)

        # 创建顶部控制区域
        control_layout = QHBoxLayout()

        # 文件路径输入框
        self.path_input = QLineEdit()
        self.path_input.setPlaceholderText('输入JSON文件路径或选择文件...')
        control_layout.addWidget(self.path_input)

        # 选择文件按钮
        self.browse_button = QPushButton('浏览...')
        self.browse_button.clicked.connect(self.browse_file)
        control_layout.addWidget(self.browse_button)

        # 加载文件按钮
        self.load_button = QPushButton('加载')
        self.load_button.clicked.connect(self.load_json)
        control_layout.addWidget(self.load_button)

        main_layout.addLayout(control_layout)

        # 创建树状视图区域
        self.tree_widget = QTreeWidget()
        self.tree_widget.setHeaderLabels(['键/索引', '类型', '值预览'])
        self.tree_widget.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.tree_widget.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
        self.tree_widget.header().setSectionResizeMode(2, QHeaderView.Stretch)
        self.tree_widget.setAlternatingRowColors(True)
        self.tree_widget.setAnimated(True)

        # 自定义样式表
        self.tree_widget.setStyleSheet("""
            QTreeWidget {
                border: 1px solid #aaa;
                background-color: #f8f8f8;
            }
            QTreeWidget::item {
                padding: 4px;
                border-bottom: 1px solid #eee;
            }
            QTreeWidget::item:selected {
                background-color: #e0e0ff;
            }
        """)

        # 连接展开信号到懒加载处理器 - 移到这里,只连接一次
        self.tree_widget.itemExpanded.connect(self.on_item_expanded)

        main_layout.addWidget(self.tree_widget)

        # 设置中央窗口部件
        self.setCentralWidget(central_widget)

        # 状态栏
        self.statusBar().showMessage('准备就绪')

    def browse_file(self):
        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择JSON文件", "", "JSON文件 (*.json);;所有文件 (*)", options=options
        )
        if file_path:
            self.path_input.setText(file_path)

    def load_json(self):
        path = self.path_input.text().strip()
        if not path:
            QMessageBox.warning(self, "警告", "请输入文件路径或选择文件")
            return

        try:
            # 尝试加载JSON数据
            if os.path.isfile(path):
                with open(path, 'r', encoding='utf-8') as f:
                    self.current_json_data = json.load(f)
            else:
                # 如果不是文件路径,尝试将输入作为JSON字符串解析
                self.current_json_data = json.loads(path)

            # 清除当前树状视图
            self.tree_widget.clear()

            # 构建根节点
            root_type = self.get_type_name(self.current_json_data)
            root_item = QTreeWidgetItem(self.tree_widget,
                                        ['根元素', root_type, self.get_preview(self.current_json_data)])

            # 使用标准方法添加子节点
            self.add_children(root_item, self.current_json_data)

            # 展开根节点
            root_item.setExpanded(True)

            self.statusBar().showMessage(f'成功加载JSON数据')
        except Exception as e:
            QMessageBox.critical(self, "错误", f"加载JSON数据失败: {str(e)}")
            self.statusBar().showMessage('加载失败')

    def add_children(self, parent_item, data, start_index=0):
        """递归添加子节点,支持分页"""
        parent_item.takeChildren()  # 清除现有子节点以重新加载

        if isinstance(data, dict):
            keys = list(data.keys())
            total_items = len(keys)

            # 显示分页信息
            if total_items > 0:
                end_index = min(start_index + self.page_size, total_items)

                # 添加分页控制按钮
                if total_items > self.page_size:
                    # 计算当前页和总页数
                    max_page = (total_items - 1) // self.page_size + 1
                    current_page = start_index // self.page_size + 1

                    # 添加分页控制组件
                    pagination_item = QTreeWidgetItem(parent_item)

                    # 前两列保留为空
                    pagination_item.setText(0, "")
                    pagination_item.setText(1, "")

                    # 创建导航控件的容器
                    navigation_widget = QWidget()
                    navigation_layout = QHBoxLayout(navigation_widget)
                    navigation_layout.setContentsMargins(5, 2, 5, 2)
                    navigation_layout.setSpacing(8)
                    navigation_layout.setAlignment(Qt.AlignLeft)  # 确保所有控件靠左对齐

                    # 设置固定高度
                    navigation_widget.setFixedHeight(30)

                    # 左边的翻页按钮
                    prev_btn = QPushButton("◀")
                    prev_btn.setFixedSize(22, 22)
                    prev_btn.setEnabled(start_index > 0)
                    prev_btn.clicked.connect(lambda: self.change_page(parent_item, data, start_index - self.page_size))

                    # 右边的翻页按钮
                    next_btn = QPushButton("▶")
                    next_btn.setFixedSize(22, 22)
                    next_btn.setEnabled(end_index < total_items)
                    next_btn.clicked.connect(lambda: self.change_page(parent_item, data, start_index + self.page_size))

                    # 跳转按钮
                    goto_btn = QPushButton("跳转")
                    goto_btn.setFixedWidth(40)
                    goto_btn.setFixedHeight(22)

                    # 固定宽度的输入框
                    goto_input = QLineEdit()
                    goto_input.setFixedWidth(40)
                    goto_input.setFixedHeight(22)
                    goto_input.setPlaceholderText("页码")

                    # 创建页码显示标签
                    page_info = f"{current_page} / {max_page}"
                    page_label = QLabel(page_info)
                    page_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)

                    # 安全的处理函数
                    def create_safe_jump_handler(parent, data_ref, input_field, max_pg):
                        p_item = parent
                        d = data_ref
                        m_page = max_pg

                        def handler():
                            text = input_field.text()
                            try:
                                self.safe_jump_to_page(p_item, d, text, m_page)
                            except Exception as e:
                                print(f"跳转处理中发生错误: {str(e)}")
                                pass

                        return handler

                    # 创建并连接安全的处理函数
                    safe_handler = create_safe_jump_handler(parent_item, data, goto_input, max_page)
                    goto_btn.clicked.connect(safe_handler)
                    goto_input.returnPressed.connect(safe_handler)

                    # 按照指定的顺序添加组件到布局中
                    navigation_layout.addWidget(prev_btn)
                    navigation_layout.addWidget(next_btn)
                    navigation_layout.addWidget(goto_btn)
                    navigation_layout.addWidget(goto_input)
                    navigation_layout.addWidget(page_label)
                    navigation_layout.addStretch(1)  # 确保所有控件靠左

                    # 将导航控件设置在第三列(值预览)
                    self.tree_widget.setItemWidget(pagination_item, 2, navigation_widget)

                # 添加数据项
                for i, key in enumerate(keys[start_index:end_index]):
                    value = data[key]
                    value_type = self.get_type_name(value)
                    preview = self.get_preview(value)

                    child = QTreeWidgetItem(parent_item, [str(key), value_type, preview])

                    # 如果值是可展开的(字典或列表),则添加一个临时子项
                    if isinstance(value, (dict, list)) and value:
                        QTreeWidgetItem(child, ["加载中...", "", ""])
                        child.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)

                # 如果有大量项,添加一个汇总信息行
                if total_items > end_index:
                    remaining = total_items - end_index
                    if remaining > 0:
                        summary_item = QTreeWidgetItem(parent_item, ["...", "", f"【剩余 {remaining} 项】"])
                        summary_item.setFlags(summary_item.flags() & ~Qt.ItemIsSelectable)
                        font = summary_item.font(0)
                        font.setItalic(True)
                        summary_item.setFont(0, font)

        elif isinstance(data, list):
            total_items = len(data)

            # 显示分页信息
            if total_items > 0:
                end_index = min(start_index + self.page_size, total_items)

                # 添加分页控制按钮
                if total_items > self.page_size:
                    # 计算当前页和总页数
                    max_page = (total_items - 1) // self.page_size + 1
                    current_page = start_index // self.page_size + 1

                    # 添加分页控制组件
                    pagination_item = QTreeWidgetItem(parent_item)

                    # 前两列保留为空
                    pagination_item.setText(0, "")
                    pagination_item.setText(1, "")

                    # 创建导航控件的容器
                    navigation_widget = QWidget()
                    navigation_layout = QHBoxLayout(navigation_widget)
                    navigation_layout.setContentsMargins(5, 2, 5, 2)
                    navigation_layout.setSpacing(8)
                    navigation_layout.setAlignment(Qt.AlignLeft)  # 确保所有控件靠左对齐

                    # 设置固定高度
                    navigation_widget.setFixedHeight(30)

                    # 左边的翻页按钮
                    prev_btn = QPushButton("◀")
                    prev_btn.setFixedSize(22, 22)
                    prev_btn.setEnabled(start_index > 0)
                    prev_btn.clicked.connect(lambda: self.change_page(parent_item, data, start_index - self.page_size))

                    # 右边的翻页按钮
                    next_btn = QPushButton("▶")
                    next_btn.setFixedSize(22, 22)
                    next_btn.setEnabled(end_index < total_items)
                    next_btn.clicked.connect(lambda: self.change_page(parent_item, data, start_index + self.page_size))

                    # 跳转按钮
                    goto_btn = QPushButton("跳转")
                    goto_btn.setFixedWidth(40)
                    goto_btn.setFixedHeight(22)

                    # 固定宽度的输入框
                    goto_input = QLineEdit()
                    goto_input.setFixedWidth(40)
                    goto_input.setFixedHeight(22)
                    goto_input.setPlaceholderText("页码")

                    # 创建页码显示标签
                    page_info = f"{current_page} / {max_page}"
                    page_label = QLabel(page_info)
                    page_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)

                    # 安全的处理函数
                    def create_safe_jump_handler(parent, data_ref, input_field, max_pg):
                        p_item = parent
                        d = data_ref
                        m_page = max_pg

                        def handler():
                            text = input_field.text()
                            try:
                                self.safe_jump_to_page(p_item, d, text, m_page)
                            except Exception as e:
                                print(f"跳转处理中发生错误: {str(e)}")
                                pass

                        return handler

                    # 创建并连接安全的处理函数
                    safe_handler = create_safe_jump_handler(parent_item, data, goto_input, max_page)
                    goto_btn.clicked.connect(safe_handler)
                    goto_input.returnPressed.connect(safe_handler)

                    # 按照指定的顺序添加组件到布局中
                    navigation_layout.addWidget(prev_btn)
                    navigation_layout.addWidget(next_btn)
                    navigation_layout.addWidget(goto_btn)
                    navigation_layout.addWidget(goto_input)
                    navigation_layout.addWidget(page_label)
                    navigation_layout.addStretch(1)  # 确保所有控件靠左

                    # 将导航控件设置在第三列(值预览)
                    self.tree_widget.setItemWidget(pagination_item, 2, navigation_widget)

                # 添加数据项 - 全部显示当前页的所有项目
                for i, value in enumerate(data[start_index:end_index]):
                    value_type = self.get_type_name(value)
                    preview = self.get_preview(value)
                    child = QTreeWidgetItem(parent_item, [f"[{start_index + i}]", value_type, preview])

                    # 如果值是可展开的(字典或列表),则添加一个临时子项
                    if isinstance(value, (dict, list)) and value:
                        QTreeWidgetItem(child, ["加载中...", "", ""])
                        child.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)

                # 如果有大量项,添加一个汇总信息行
                if total_items > end_index:
                    remaining = total_items - end_index
                    if remaining > 0:
                        summary_item = QTreeWidgetItem(parent_item, ["...", "", f"【剩余 {remaining} 项】"])
                        summary_item.setFlags(summary_item.flags() & ~Qt.ItemIsSelectable)
                        font = summary_item.font(0)
                        font.setItalic(True)
                        summary_item.setFont(0, font)

        # 移除这里的信号连接,避免重复连接
        # self.tree_widget.itemExpanded.connect(self.on_item_expanded)

    def change_page(self, parent_item, data, new_start_index):
        """更改分页索引并重新加载子项"""
        # 确保索引有效
        if isinstance(data, (list, dict)):
            total_items = len(data)
            new_start_index = max(0, min(new_start_index, total_items - 1))
            self.add_children(parent_item, data, new_start_index)
            parent_item.setExpanded(True)

    def on_item_expanded(self, item):
        """当项目被展开时,懒加载子项"""
        # 检查是否是第一次展开该项
        if item.childCount() == 1 and item.child(0).text(0) == "加载中...":
            # 获取完整路径以访问数据
            path = self.get_item_path(item)
            data = self.get_data_at_path(path)

            # 清除"加载中..."占位符
            item.takeChildren()

            # 添加实际子项
            self.add_children(item, data)

    def get_item_path(self, item):
        """获取树项的路径"""
        path = []
        current = item

        # 检查是否是"加载中..."项
        if current.text(0) == "加载中...":
            current = current.parent()

        # 构建路径
        while current is not None:
            # 检查是否为根节点 (根元素)
            if current.parent() is None:
                break

            # 检查是否为分页控件或标签
            if not current.text(0) or current.text(0) in ["...", "上一页", "下一页"]:
                current = current.parent()
                continue

            # 获取键或索引
            key_text = current.text(0)
            if key_text.startswith('[') and key_text.endswith(']'):  # 列表索引
                try:
                    # 从 "[0]" 格式中提取数字
                    index = int(key_text[1:-1])
                    path.append(index)
                except ValueError:
                    # 如果无法转换为整数
                    if key_text != "根元素":  # 跳过根元素
                        path.append(key_text)
            else:
                # 跳过特殊节点
                if key_text not in ["根元素", "汇总"]:
                    path.append(key_text)

            current = current.parent()

        # 反转路径以获得正确的顺序
        return list(reversed(path))

    def get_data_at_path(self, path):
        """根据路径获取数据"""
        data = self.current_json_data
        for key in path:
            if isinstance(data, dict):
                # 检查是否是"汇总"节点
                if key == "汇总" and isinstance(data, dict):
                    return data
                data = data.get(key)
            elif isinstance(data, list) and isinstance(key, int) and 0 <= key < len(data):
                data = data[key]
            else:
                return None
        return data

    def get_type_name(self, value):
        """获取值的类型名称"""
        if isinstance(value, dict):
            return f"dict ({len(value)} keys)"
        elif isinstance(value, list):
            return f"list ({len(value)} items)"
        elif isinstance(value, tuple):
            return f"tuple ({len(value)} items)"
        elif isinstance(value, str):
            return f"str ({len(value)} chars)"
        elif isinstance(value, bool):
            return "bool"  # Changed from "布尔值"
        elif isinstance(value, int):
            return "int"  # Changed from "整数"
        elif isinstance(value, float):
            return "float"  # Changed from "浮点数"
        elif value is None:
            return "None"  # Changed from "空值"
        else:
            # For any other type, return the actual type name
            return type(value).__name__

    def get_preview(self, value, max_length=50):
        """获取值的预览文本"""
        if isinstance(value, dict):
            if not value:
                return "{}"
            # 修改:如果字典数量超过10个条目,只显示概要信息
            if len(value) > 10:
                return f"{{ ... }} (共 {len(value)} 个键值对)"
            preview = ", ".join([f"{k}: {self.get_short_preview(v)}" for k, v in list(value.items())[:3]])
            if len(value) > 3:
                preview += f", ... (还有{len(value) - 3}项)"
            return "{" + preview + "}"
        elif isinstance(value, list):
            if not value:
                return "[]"
            preview = ", ".join([self.get_short_preview(item) for item in value[:3]])
            if len(value) > 3:
                preview += f", ... (还有{len(value) - 3}项)"
            return "[" + preview + "]"
        elif isinstance(value, str):
            # 修改:对于长字符串使用换行而不是省略号
            if len(value) > max_length:
                # 返回带换行的预览,最多显示3行
                lines = value.splitlines()
                if not lines:
                    # 如果没有换行符,手动分行
                    lines = [value[i:i + max_length] for i in range(0, min(3 * max_length, len(value)), max_length)]

                if len(lines) > 3:
                    preview = "\n".join(lines[:3]) + f"\n... (还有 {len(lines) - 3} 行)"
                else:
                    preview = "\n".join(lines)

                return f'"{preview}"'
            return f'"{value}"'
        else:
            return str(value)

    def get_short_preview(self, value, max_length=20):
        """获取值的简短预览,用于嵌套展示"""
        if isinstance(value, dict):
            # 修改:如果字典数量超过10个条目,只显示概要信息
            if len(value) > 10:
                return f"{{...}} ({len(value)}键)"
            return f"{{...}} ({len(value)}项)"
        elif isinstance(value, list):
            return f"[...] ({len(value)}项)"
        elif isinstance(value, str):
            # 修改:使用换行而不是省略号
            if len(value) > max_length:
                return f'"{value[:max_length]}\\n..."'
            return f'"{value}"'
        else:
            return str(value)

    def safe_jump_to_page(self, parent_item, data, page_text, max_page):
        """安全地跳转到指定页码,包含完整的错误处理"""
        try:
            # 移除所有非数字字符 (保留纯数字)
            cleaned_text = ''.join(c for c in page_text if c.isdigit())

            # 检查输入是否为空
            if not cleaned_text:
                QMessageBox.warning(self, "警告", "请输入有效的页码")
                return

            # 尝试将输入转换为整数
            page_num = int(cleaned_text)

            # 验证页码范围
            if page_num < 1:
                QMessageBox.warning(self, "警告", "页码必须大于0")
                return
            elif page_num > max_page:
                QMessageBox.warning(self, "警告", f"页码超出范围,最大页码为: {max_page}")
                return

            # 计算开始索引
            start_index = (page_num - 1) * self.page_size

            # 变更页面
            self.change_page(parent_item, data, start_index)

        except ValueError as e:
            # 如果输入不是有效的数字
            QMessageBox.warning(self, "警告", f"请输入有效的页码数字: {str(e)}")

        except Exception as e:
            # 捕获所有可能的异常以防止应用崩溃
            error_msg = f"跳转页面时发生错误: {str(e)}"
            print(error_msg)  # 在控制台打印错误信息
            QMessageBox.critical(self, "错误", error_msg)

        # 无论如何都返回,不抛出异常
        return None

        def safe_jump_to_page(self, parent_item, data, page_text, max_page):
            """安全地跳转到指定页码,包含完整的错误处理"""

        try:
            # 移除所有非数字字符 (保留纯数字)
            cleaned_text = ''.join(c for c in page_text if c.isdigit())

            # 检查输入是否为空
            if not cleaned_text:
                QMessageBox.warning(self, "警告", "请输入有效的页码")
                return

            # 尝试将输入转换为整数
            page_num = int(cleaned_text)

            # 验证页码范围
            if page_num < 1:
                QMessageBox.warning(self, "警告", "页码必须大于0")
                return
            elif page_num > max_page:
                QMessageBox.warning(self, "警告", f"页码超出范围,最大页码为: {max_page}")
                return

            # 计算开始索引
            start_index = (page_num - 1) * self.page_size

            # 变更页面
            self.change_page(parent_item, data, start_index)

        except ValueError as e:
            # 如果输入不是有效的数字
            QMessageBox.warning(self, "警告", f"请输入有效的页码数字: {str(e)}")

        except Exception as e:
            # 捕获所有可能的异常以防止应用崩溃
            error_msg = f"跳转页面时发生错误: {str(e)}"
            print(error_msg)  # 在控制台打印错误信息
            QMessageBox.critical(self, "错误", error_msg)

        # 无论如何都返回,不抛出异常
        return None

    def jump_to_page(self, parent_item, data, page_text, max_page):
        """跳转到指定页码"""

        try:
            # 检查输入是否为空
            if not page_text or page_text.strip() == "":
                QMessageBox.warning(self, "警告", "请输入页码")
                return

            # 尝试将输入转换为整数
            page_num = int(page_text.strip())

            # 验证页码范围
            if page_num < 1 or page_num > max_page:
                QMessageBox.warning(self, "警告", f"页码超出范围,有效范围: 1-{max_page}")
                return

            # 计算开始索引
            start_index = (page_num - 1) * self.page_size

            # 变更页面
            self.change_page(parent_item, data, start_index)
        except ValueError:
            # 如果输入不是有效的数字
            QMessageBox.warning(self, "警告", "请输入有效的页码数字")
        except Exception as e:
            # 捕获所有可能的异常以防止应用崩溃
            QMessageBox.critical(self, "错误", f"跳转页面时发生错误: {str(e)}")
            print(f"跳转页面时发生错误: {str(e)}")  # 同时在控制台打印错误信息#!/usr/bin/env python3

    def is_homogeneous_list(self, data_list):
        """检查列表是否包含同类型的数据
        注意:不再使用此功能,我们将显示所有项目
        保留此方法是为了代码兼容性"""
        return False  # 直接返回False,始终显示所有项目


def main():
    app = QApplication(sys.argv)
    app.setStyle('Fusion')  # 使用Fusion风格以获得更好的跨平台外观
    ex = JSONExplorer()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

你可能感兴趣的:(项目,python,json,信息可视化)