ARTICLE · 1278685
《还在一个个Excel点开插图表?这款 Python工具一键批量生成自定义图表,彻底解放双手!!!》



“带你横跨办公自动化的数据江海”
@摸鱼
前言
关于摸鱼
(
ABOUT MOYU
)
闻道有先后,术业有专攻。
各位大佬朋友们好!
~我依旧是你们的老朋友摸鱼~
在职场摸爬滚打的这十多年里,我用Python悄悄干了不少“正事”——不知不觉攒下了一整套办公自动化的实用项目技巧。去年10月初创立了公众号 「码海听潮」 ,初衷很简单:把重复的劳动交给代码,把摸鱼的时间留给生活。
目前已经吭哧吭哧更新了170余篇原创文章,每一篇都是实操干货,不讲虚的,只聊怎么用代码真正解放双手,帮大家早点下班、准点摸鱼。
好了,多了不说,少了不唠,今天给大佬们分享一款本人原创的【Excel 批量图表生成工具】。日常做数据汇报、台账整理的朋友,常遇到一个痛点:批量导出的 Excel 每个文件、每张表都要配一张格式统一的图表,字段位置还不一样,只能一个个点开手动插图表、调样式,几十上百个文件弄下来又费眼又容易格式不统一。
这款工具正是为了终结"批量出图全靠手动搬砖"。它能自动扫描你选的整个文件夹,一键批量处理所有 Excel,逐表智能识别数据范围(自动计算或手动指定单/多区域),按选定的图表类型(柱形、折线、饼图、散点、雷达、股价、曲面及 3D 版本等)自动插图,并统一套用标题、图例、内置样式、柱间距、数据标签、标记、趋势线、系列配色和坐标轴网格等样式;全程后台无界面生成、自动保存,首张图还能导出 PNG 在"预览"页直接看,处理情况实时写入"运行日志",成功失败一目了然,彻底解放双手!

《该办公工具成品展示:》


《该办公工具操作演示视频:》

《该办公工具核心功能亮点:》
01
亮点一:智能识别数据范围
留空即自动计算每张表的非空数据区域;也支持手动指定单区域,或用逗号分隔指定多区域(优先级最高),适配不同模板。
02
亮点二:图表类型丰富
内置柱形、条形、折线、面积、饼图、圆环、散点、气泡、雷达、股价、曲面、圆锥/圆柱/棱锥等 20+ 种类型,勾选"3D 效果"自动映射到对应的 3D 图表类型。
03
亮点三:样式统一可配
标题(字号/加粗)、图例位置与叠加、内置样式编号、柱间距与系列重叠、折线平滑、数据标签及位置、数据点标记、趋势线、系列自动轮换配色,以及分类轴/数值轴标题、刻度标签位置、主次网格线、数值轴自动缩放或手动上下限、对数刻度、反转分类轴——一次设置,全部文档统一套用。

《该办公工具代码展示:》
下面,我就奉上python代码让各位大佬见识一下,什么叫"传统文化"遇上"赛博效率"
import sysfrom PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QFormLayout, QGroupBox, QLineEdit, QComboBox, QSpinBox,QPushButton, QLabel, QDoubleSpinBox,QCheckBox, QTextEdit, QScrollArea, QTabWidget,QRadioButton, QButtonGroup, QListWidget,QListWidgetItem)from PyQt6.QtCore import Qt, QTimer, QSettingsfrom PyQt6.QtGui import QColor, QPixmapclass ExcelChartTool(QMainWindow):def __init__(self):super().__init__()self._last_pixmap = Noneself._selected_color = Noneself._resize_timer = QTimer(self)self._resize_timer.setSingleShot(True)self._resize_timer.setInterval(200)self._resize_timer.timeout.connect(self._rescale_cached_pixmap)self.setWindowTitle("Excel自定义图表批量生成工具(欢迎关注微信公众号:码海听潮)")self.setGeometry(80, 80, 1180, 900)self._build_ui()self._settings = QSettings("ExcelChartTool", "UI")last_theme = self._settings.value("theme", "默认(浅色)")idx = self.theme_combo.findText(last_theme)if idx >= 0:self.theme_combo.setCurrentIndex(idx)self._apply_theme(self.theme_combo.currentText())# ==================== UI 构建 ====================def _build_ui(self):central = QWidget()self.setCentralWidget(central)main_layout = QVBoxLayout(central)theme_bar = QHBoxLayout()theme_bar.addWidget(QLabel("界面主题:"))self.theme_combo = QComboBox()self.theme_combo.addItems(["默认(浅色)", "深色", "蓝色", "绿色", "紫色", "暖橙"])self.theme_combo.currentTextChanged.connect(self._on_theme_changed)theme_bar.addWidget(self.theme_combo)theme_bar.addStretch(1)main_layout.addLayout(theme_bar)self.tabs = QTabWidget()self.tabs.addTab(self._build_data_tab(), "1. 数据源")self.tabs.addTab(self._build_chart_tab(), "2. 图表类型与样式")self.tabs.addTab(self._build_series_tab(), "3. 系列与标签")self.tabs.addTab(self._build_axis_tab(), "4. 坐标轴与网格")self.tabs.addTab(self._build_preview_tab(), "5. 预览")self.tabs.addTab(self._build_log_tab(), "6. 运行日志")main_layout.addWidget(self.tabs)btn_layout = QHBoxLayout()create_btn = QPushButton("创建 / 更新图表(后台生成)")create_btn.clicked.connect(self._create_chart)create_btn.setStyleSheet("font-weight: bold; padding: 8px;")btn_layout.addWidget(create_btn)main_layout.addLayout(btn_layout)def _on_theme_changed(self, name):self._apply_theme(name)try:self._settings.setValue("theme", name)except Exception:passdef _apply_theme(self, name):theme = THEMES.get(name, THEMES["默认(浅色)"])qss = """QWidget {{font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;font-size: 13px;color: {fg};background-color: {bg};}}QMainWindow, QDialog {{background-color: {bg};}}QTabWidget::pane {{border: 1px solid {border};border-radius: 6px;background: {panel};top: -1px;}}QTabBar::tab {{background: {tab_bg};color: {fg};border: 1px solid {border};border-bottom: none;padding: 8px 16px;margin-right: 2px;border-top-left-radius: 6px;border-top-right-radius: 6px;}}QTabBar::tab:selected {{background: {panel};color: {accent};font-weight: bold;border-bottom: 2px solid {accent};}}QTabBar::tab:hover {{background: {hover};}}QGroupBox {{border: 1px solid {border};border-radius: 6px;margin-top: 10px;padding-top: 10px;background: {panel};font-weight: bold;}}QGroupBox::title {{subcontrol-origin: margin;subcontrol-position: top left;left: 12px;padding: 0 6px;color: {accent};}}QPushButton {{background: {btn_bg};color: {btn_fg};border: 1px solid {border};border-radius: 6px;padding: 6px 14px;min-height: 18px;}}QPushButton:hover {{background: {btn_hover};}}QPushButton:pressed {{background: {accent};color:#ffffff;}}QPushButton:disabled {{color: {disabled};background: {tab_bg};}}QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox, QTextEdit,QListWidget {{background: {input_bg};color: {fg};border: 1px solid {border};border-radius: 4px;padding: 4px 6px;selection-background-color: {accent};selection-color:#ffffff;}}QLineEdit:focus, QComboBox:focus, QSpinBox:focus,QDoubleSpinBox:focus, QTextEdit:focus, QListWidget:focus {{border: 1px solid {accent};}}QComboBox::drop-down {{border: none;width: 22px;}}QComboBox QAbstractItemView {{background: {input_bg};color: {fg};border: 1px solid {border};selection-background-color: {accent};selection-color:#ffffff;}}QScrollArea {{border: 1px solid {border};border-radius: 6px;background: {panel};}}QCheckBox {{spacing: 6px;}}QCheckBox::indicator {{width: 16px;height: 16px;border: 1px solid {border};border-radius: 3px;background: {input_bg};}}QCheckBox::indicator:checked {{background: {accent};border: 1px solid {accent};}}QScrollBar:vertical {{background: {bg};width: 12px;margin: 0;}}QScrollBar::handle:vertical {{background: {border};border-radius: 6px;min-height: 24px;}}QScrollBar::handle:vertical:hover {{background: {accent};}}QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{height: 0;}}QLabel {{background: transparent;}}"""self.setStyleSheet(qss.format(**theme))# ---------- 数据源标签页 ----------def _build_data_tab(self):w = QWidget()outer = QVBoxLayout(w)mode_group = QGroupBox("数据源模式")mode_layout = QHBoxLayout()self.mode_file_rb = QRadioButton("单个 Excel 文件")self.mode_folder_rb = QRadioButton("Excel 文件夹")self.mode_file_rb.setChecked(True)self._mode_group = QButtonGroup(self)self._mode_group.addButton(self.mode_file_rb)self._mode_group.addButton(self.mode_folder_rb)self.mode_file_rb.toggled.connect(self._on_mode_changed)mode_layout.addWidget(self.mode_file_rb)mode_layout.addWidget(self.mode_folder_rb)mode_layout.addStretch(1)mode_group.setLayout(mode_layout)outer.addWidget(mode_group)self.file_group = QGroupBox("Excel 文件")file_layout = QHBoxLayout()self.file_path_edit = QLineEdit()self.file_path_edit.setPlaceholderText("选择 .xlsx / .xlsm 文件…")browse_btn = QPushButton("浏览")browse_btn.clicked.connect(self._browse_file)refresh_btn = QPushButton("刷新工作表列表")refresh_btn.clicked.connect(self._refresh_sheets)file_layout.addWidget(self.file_path_edit)file_layout.addWidget(browse_btn)file_layout.addWidget(refresh_btn)self.file_group.setLayout(file_layout)outer.addWidget(self.file_group)self.folder_group = QGroupBox("Excel 文件夹(查找 .xlsx / .xlsm)")folder_layout = QHBoxLayout()self.folder_path_edit = QLineEdit()self.folder_path_edit.setPlaceholderText("选择包含 Excel 文件的文件夹…")folder_browse_btn = QPushButton("浏览")folder_browse_btn.clicked.connect(self._browse_folder)folder_layout.addWidget(self.folder_path_edit)folder_layout.addWidget(folder_browse_btn)self.folder_group.setLayout(folder_layout)outer.addWidget(self.folder_group)# —— 工作表多选区(QLineEdit 显示 + QListWidget 勾选)——self.sheet_group = QGroupBox("工作表(可多选)")sg_layout = QVBoxLayout()self.sheet_display_edit = QLineEdit()self.sheet_display_edit.setReadOnly(True)self.sheet_display_edit.setPlaceholderText("(未选择工作表)")self.sheet_display_edit.setText("(未选择工作表)")self.sheet_list = QListWidget()self.sheet_list.setMaximumHeight(150)self.sheet_list.itemChanged.connect(self._on_sheet_item_changed)self.sheet_list.itemClicked.connect(self._on_sheet_item_clicked)sg_layout.addWidget(self.sheet_display_edit)sg_layout.addWidget(self.sheet_list)self.sheet_group.setLayout(sg_layout)outer.addWidget(self.sheet_group)form = QFormLayout()self.range_edit = QLineEdit()self.range_edit.setPlaceholderText("留空自动计算;或填 A1:D10 强制指定")self.multi_range_edit = QLineEdit()self.multi_range_edit.setPlaceholderText("可选:多区域用逗号分隔,例如 A1:D10,A1:E10(优先级最高)")form.addRow("数据范围:", self.range_edit)form.addRow("多区域(可选):", self.multi_range_edit)outer.addLayout(form)self._on_mode_changed()outer.addStretch(1)return wdef _on_mode_changed(self):is_file = self.mode_file_rb.isChecked()self.file_group.setVisible(is_file)self.folder_group.setVisible(not is_file)self.sheet_group.setVisible(is_file)# ---------- 工作表多选辅助 ----------def _on_sheet_item_changed(self, item):self._update_sheet_display_text()def _on_sheet_item_clicked(self, item):new_state = (Qt.CheckState.Uncheckedif item.checkState() == Qt.CheckState.Checkedelse Qt.CheckState.Checked)item.setCheckState(new_state)def _update_sheet_display_text(self):checked = []for i in range(self.sheet_list.count()):it = self.sheet_list.item(i)if it and it.checkState() == Qt.CheckState.Checked:checked.append(it.text())text = "、".join(checked) if checked else "(未选择工作表)"self.sheet_display_edit.setText(text)def _get_checked_sheets(self):checked = []for i in range(self.sheet_list.count()):it = self.sheet_list.item(i)if it and it.checkState() == Qt.CheckState.Checked:checked.append(it.text())return checked# ---------- 图表类型与样式 ----------def _build_chart_tab(self):w = QWidget()layout = QFormLayout(w)self.chart_type_combo = QComboBox()for name, val in CHART_TYPES:self.chart_type_combo.addItem(name, val)self.title_edit = QLineEdit("数据图表")self.title_size_spin = QSpinBox()self.title_size_spin.setRange(6, 60)self.title_size_spin.setValue(16)self.title_bold_cb = QCheckBox("标题加粗")self.title_bold_cb.setChecked(True)self.show_legend_cb = QCheckBox("显示图例")self.show_legend_cb.setChecked(True)self.legend_combo = QComboBox()for name, val in LEGEND_POSITIONS:self.legend_combo.addItem(name, val)self.style_spin = QSpinBox()self.style_spin.setRange(1, 48)self.style_spin.setValue(10)self.smooth_cb = QCheckBox("折线/散点平滑")self.show_3d_cb = QCheckBox("3D 效果(自动切换为 3D 图表类型)")self.legend_overlay_cb = QCheckBox("图例叠加到图表上")self.gap_spin = QSpinBox()self.gap_spin.setRange(0, 500)self.gap_spin.setValue(150)self.overlap_spin = QSpinBox()self.overlap_spin.setRange(-100, 100)self.overlap_spin.setValue(0)self.chart_left_spin = QDoubleSpinBox(); self.chart_left_spin.setRange(0, 3000); self.chart_left_spin.setValue(400)self.chart_top_spin = QDoubleSpinBox(); self.chart_top_spin.setRange(0, 3000); self.chart_top_spin.setValue(20)self.chart_width_spin = QDoubleSpinBox(); self.chart_width_spin.setRange(100, 2000); self.chart_width_spin.setValue(500)self.chart_height_spin = QDoubleSpinBox(); self.chart_height_spin.setRange(100, 2000); self.chart_height_spin.setValue(400)layout.addRow("图表类型:", self.chart_type_combo)layout.addRow("图表标题:", self.title_edit)layout.addRow("标题字号:", self.title_size_spin)layout.addRow("", self.title_bold_cb)layout.addRow("", self.show_legend_cb)layout.addRow("图例位置:", self.legend_combo)layout.addRow("", self.legend_overlay_cb)layout.addRow("内置样式编号:", self.style_spin)layout.addRow("", self.smooth_cb)layout.addRow("", self.show_3d_cb)layout.addRow("柱间距:", self.gap_spin)layout.addRow("系列重叠:", self.overlap_spin)layout.addRow("位置/尺寸:", self._make_position_widget())return w# ---------- 系列与标签 ----------def _build_series_tab(self):w = QWidget()layout = QFormLayout(w)self.label_type_combo = QComboBox()for name, val in LABELS:self.label_type_combo.addItem(name, val)self.label_pos_combo = QComboBox()label_pos = [("自动", -4105), ("内部", 2), ("外部", 3), ("居中", -4108),("上部", 0), ("下部", 4), ("左侧", 5), ("右侧", 6)]for name, val in label_pos:self.label_pos_combo.addItem(name, val)self.marker_combo = QComboBox()for name, val in MARKERS:self.marker_combo.addItem(name, val)self.trendline_combo = QComboBox()for name, val in TRENDLINES:self.trendline_combo.addItem(name, val)self.trend_display_cb = QCheckBox("趋势线显示公式")self.trend_r2_cb = QCheckBox("趋势线显示 R²")self.series_color_btn = QPushButton("为所有系列设置颜色…")self.series_color_btn.clicked.connect(self._pick_series_color)self.series_color_label = QLabel("未选择颜色")self.per_series_color_cb = QCheckBox("每个系列使用不同颜色(自动轮换)")self.per_series_color_cb.setChecked(True)layout.addRow("数据标签:", self.label_type_combo)layout.addRow("标签位置:", self.label_pos_combo)layout.addRow("数据点标记:", self.marker_combo)layout.addRow("趋势线类型:", self.trendline_combo)layout.addRow("", self.trend_display_cb)layout.addRow("", self.trend_r2_cb)layout.addRow("", self.per_series_color_cb)layout.addRow(self.series_color_btn, self.series_color_label)return w# ---------- 坐标轴与网格 ----------def _build_axis_tab(self):w = QWidget()layout = QFormLayout(w)self.cat_title_edit = QLineEdit("类别")self.val_title_edit = QLineEdit("数值")self.cat_title_size_spin = QSpinBox(); self.cat_title_size_spin.setRange(6, 40); self.cat_title_size_spin.setValue(11)self.val_title_size_spin = QSpinBox(); self.val_title_size_spin.setRange(6, 40); self.val_title_size_spin.setValue(11)self.cat_tick_combo = QComboBox()for name, val in TICK_POS:self.cat_tick_combo.addItem(name, val)self.val_tick_combo = QComboBox()for name, val in TICK_POS:self.val_tick_combo.addItem(name, val)self.grid_major_x_cb = QCheckBox("显示主要横网格线"); self.grid_major_x_cb.setChecked(True)self.grid_major_y_cb = QCheckBox("显示主要纵网格线"); self.grid_major_y_cb.setChecked(True)self.grid_minor_x_cb = QCheckBox("显示次要横网格线")self.grid_minor_y_cb = QCheckBox("显示次要纵网格线")self.val_min_spin = QDoubleSpinBox(); self.val_min_spin.setRange(-99999, 99999); self.val_min_spin.setValue(0)self.val_max_spin = QDoubleSpinBox(); self.val_max_spin.setRange(-99999, 99999); self.val_max_spin.setValue(1000)self.auto_scale_cb = QCheckBox("数值轴自动缩放"); self.auto_scale_cb.setChecked(True)self.log_scale_cb = QCheckBox("数值轴对数刻度")self.reverse_cat_cb = QCheckBox("反转分类轴顺序")layout.addRow("分类轴标题:", self.cat_title_edit)layout.addRow("分类轴字号:", self.cat_title_size_spin)layout.addRow("分类轴标签位置:", self.cat_tick_combo)layout.addRow("数值轴标题:", self.val_title_edit)layout.addRow("数值轴字号:", self.val_title_size_spin)layout.addRow("数值轴标签位置:", self.val_tick_combo)layout.addRow("", self.grid_major_x_cb)layout.addRow("", self.grid_major_y_cb)layout.addRow("", self.grid_minor_x_cb)layout.addRow("", self.grid_minor_y_cb)layout.addRow("", self.auto_scale_cb)layout.addRow("数值轴最小值:", self.val_min_spin)layout.addRow("数值轴最大值:", self.val_max_spin)layout.addRow("", self.log_scale_cb)layout.addRow("", self.reverse_cat_cb)return w# ---------- 预览 ----------def _build_preview_tab(self):w = QWidget()layout = QVBoxLayout(w)self.preview_label = QLabel("尚未生成图表\n\n请先在「1. 数据源」填写范围,再点「创建 / 更新图表」")self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)self.preview_label.setMinimumSize(760, 460)self.preview_label.setStyleSheet("border: 1px dashed#999; background:#fafafa; color:#888;")preview_scroll = QScrollArea()preview_scroll.setWidgetResizable(True)preview_scroll.setWidget(self.preview_label)layout.addWidget(preview_scroll, 1)return w# ---------- 运行日志标签页 ----------def _build_log_tab(self):w = QWidget()layout = QVBoxLayout(w)clear_btn = QPushButton("清空日志")clear_btn.clicked.connect(lambda: self.log_text.clear())self.log_text = QTextEdit()self.log_text.setReadOnly(True)layout.addWidget(clear_btn)layout.addWidget(self.log_text, 1)return wdef _make_position_widget(self):w = QWidget()layout = QHBoxLayout(w)layout.setContentsMargins(0, 0, 0, 0)layout.setSpacing(4)for spin in (self.chart_left_spin, self.chart_top_spin,self.chart_width_spin, self.chart_height_spin):spin.setFixedWidth(100)for label, spin in [("左", self.chart_left_spin), ("上", self.chart_top_spin),("宽", self.chart_width_spin), ("高", self.chart_height_spin)]:layout.addWidget(QLabel(label))layout.addWidget(spin)layout.addStretch(1)return w# ==================== 界面辅助行为 ====================def _log(self, msg):self.log_text.append(msg)QApplication.processEvents()def _rescale_cached_pixmap(self):if not self._last_pixmap:returntarget_w = max(self.preview_label.width() - 4, 100)target_h = max(self.preview_label.height() - 4, 100)self.preview_label.setPixmap(self._last_pixmap.scaled(target_w, target_h,Qt.AspectRatioMode.KeepAspectRatio,Qt.TransformationMode.SmoothTransformation,))def resizeEvent(self, event):super().resizeEvent(event)self._resize_timer.start()# ==================== 占位槽(原业务逻辑已移除)====================def _browse_file(self):self._log("(界面演示)选择单个 Excel 文件")def _browse_folder(self):self._log("(界面演示)选择 Excel 文件夹")def _refresh_sheets(self):self._log("(界面演示)刷新工作表列表")def _pick_series_color(self):self._log("(界面演示)选择系列颜色")def _create_chart(self):self._log("(界面演示)点击了创建 / 更新图表")if __name__ == "__main__":app = QApplication(sys.argv)window = ExcelChartTool()window.show()sys.exit(app.exec())
【Excel自定义图表批量生成工具】,彻底告别逐个文件打开、逐张表手动插图调格式的繁琐。只需选定文件夹,工具即可自动完成目录扫描、数据范围识别、按所选图表类型批量插图,并统一套用标题、图例、配色、数据标签、趋势线与坐标轴等样式,智能生成后自动保存。全程后台无界面运行,无需手动干预,处理结果实时写入运行日志,效率指数级提升,轻松解放双手!
大佬们也可以举一反三,参照上面的代码思路根据自己工作中的实际情况来具体问题具体分析,实现自己定制化的需求。

《该办公工具的应用场景:》
1. 月度/季度/年度数据汇报
财务、运营、销售部门每月从系统导出大量 Excel(分部门、分区域、分产品),需要统一出一批格式一致的柱形/折线/饼图用于汇报 PPT 或报表,工具可一键批量生成。
2. 多门店 / 多分公司 / 多项目台账
连锁门店、分公司、项目部各自一张表,字段位置常有差异。工具按工作表自动识别数据范围(也可手动指定单/多区域),逐表生成统一图表,避免人工逐份调整。
3. 经营分析与对比看板
需要同一指标在多个主体间横向对比时,批量生成同类型(如堆积柱形、雷达图、股价图)图表,保证口径与样式一致,便于汇总成分析报告。
4. 数据归档与打印装订
档案、审计、统计工作中,需将历史 Excel 数据转成图表随文档打印归档,工具后台批量生成并保存,避免逐个打开操作。
一句话概括:凡是"一批结构相近的 Excel 需要出同款图表"的重复性工作,都是它的用武之地。
结语
当Python遇见办公,牛马打工人终于笑出了猪叫声~~~
【职场人必看】每天早上一睁眼,想到又要面对:
1.📊 堆积如山的Excel表格
2.📑 机械重复的复制粘贴
3.✍️ 永远改不完的各类文档
4.诸如此类的更多........
是不是连Ctrl+Alt+Delete的心都有了?
别慌!别急,摸鱼这位“职场外挂”已经带着Python代码来拯救你了!
臣无大佬,无以至今日;大佬无臣,无以摸大鱼。感谢各位大佬阅读,还望各位大佬抬抬贵手一键三连,多多关注、点赞、转发、评论——大佬们的支持,才是摸鱼孜孜不倦更新原创干货的动力















































































