这个课的主题很集中:把微信收款码和支付宝收款码合成一张能被两个 App 分别识别的二维码。它不是一个讲支付接口对接、也不涉及商户资质和结算流程的课,而是纯粹的图像与二维码编码技巧。很多人第一次听到"二合一"会以为是某种聚合支付服务,其实不是——它不碰任何支付通道,只是把两张码的信息重新排布到一张图上。理解这一点,才知道这门课真正要解决的问题边界在哪里。

它解决的是一个非常具体的场景:小摊、小店、个人收款时不想贴两张码,或者想在柜台只摆一个立牌。传统做法是把两张码并排放,用户得自己找对应 App。二合一码的思路是让同一张图里同时容纳两条编码信息,微信扫出来是微信的收款链接,支付宝扫出来是支付宝的收款链接。这背后的关键不在支付,而在二维码的容错机制、掩码选择和识别顺序。课程真正值钱的部分,就是把这个"为什么能成立"讲清楚,而不是丢一段代码让你跑。

课纲里真正有含金量的几块

从整体结构看,这门课的重心明显落在三个地方:二维码生成库的调用与参数控制、两张码的物理合并策略、以及桌面端的操作界面。生成部分用的是成熟的二维码库,这部分本身不难,难点在于你要理解版本号、纠错等级、模块尺寸这些参数怎么影响最终结果。纠错等级调高会让码更"抗污损",但模块变多、图变大;调低则相反。合并策略才是核心,常见做法有上下拼接、左右拼接、以及把一张码的有效区域嵌入另一张码的空白区,不同策略的识别成功率差别很大,课里会对比这些方案。

界面部分用的是 Python 自带的图形库,做的是文件选择、预览、保存这类操作。这块含金量中等,属于"能跑起来、能交付给非技术用户"的工程收尾。真正值得反复看的是合并逻辑那几十行,因为它决定了成品到底能不能被扫出来。很多网上流传的所谓二合一码其实是失败的,扫出来识别不了或者只认一家,差别就在合并时有没有处理好静区、有没有破坏定位图案。

适合什么基础的人看

如果你完全没写过 Python,这门课会让你有点吃力。它默认你能看懂函数、能装第三方库、能理解图像对象的基本操作。好在你不需要懂支付业务,也不需要懂网络请求,门槛主要卡在 Python 基础语法和一点图像处理常识上。会一点 PIL 或二维码相关经验的人,看这门课会非常快,可能一两个小时就能把核心逻辑吃透。

反过来说,如果你已经做过二维码生成,只是没试过合并两张码,那这门课对你最大的价值是省掉试错时间。合并这件事看起来简单,实际上静区留多少、两张码的尺寸怎么对齐、用什么方式把信息叠进去,都会影响识别。自己摸索可能要试十几次,课里直接给了可用的参数组合和调整思路。对只想快速做出一个能用的工具的人来说,这就是它最实在的地方。

建议先看哪几块,顺序怎么排

建议不要从头顺着看。先跳到合并逻辑那部分,把"一张图怎么同时装两条信息"这个原理搞明白。原理通了,再看生成参数就很容易理解每个参数在干什么。最后回头看界面部分,因为界面是壳,先看壳容易被细节淹没,抓不住重点。如果时间紧,甚至可以只看合并逻辑加生成参数两块,界面自己照着改都行。

看的时候建议边看边动手改参数。把纠错等级从默认值调高一级,看成品图变成什么样、还能不能扫出来;把两张码的尺寸比例改一改,观察识别率的变化。这种"改一个参数看一个结果"的方式,比单纯看讲解有效得多。二合一码的很多经验是手感,光看文字记不住,必须自己扫几次、失败几次,才能形成判断。

学完之后能独立做什么

最直接的产出是一个能跑的桌面小工具:选两张收款码图片,点一下生成一张合并码,保存下来就能用。你可以把它打包给不装 Python 的人用,也可以改成命令行版本批量处理。更进一步,你能把这个合并思路迁移到别的场景,比如把两张活动码、两张问卷码合成一张,原理是相通的。

但要清楚它的边界:它只做图像层面的合并,不解决支付本身的问题。收款到账、对账、退款这些它一概不管。所以学完不要指望它能帮你做聚合支付业务,它就是一个"把两张图变成一张图、且两张图都还能被认出来"的小工具。认清这一点,你就不会对它有不切实际的期待,也更能判断它值不值得花时间。

资料怎么配合练习用

资料里的代码是完整的可运行版本,建议先原样跑通一次,确认环境没问题、能生成出图。跑通之后再开始拆:把生成部分和合并部分分开,各自单独运行,看中间产物长什么样。中间产物是理解合并逻辑的关键,很多人跳过这一步直接看最终结果,结果出了问题完全不知道是哪一步坏的。把中间图存下来对比,是最高效的调试方式。

练习素材建议自己准备两张真实的收款码,不要用网上随便找的图,因为不同来源的码尺寸、留白、清晰度都不一样,正好用来测试合并逻辑的鲁棒性。生成后用微信和支付宝各扫几次,记录成功和失败的情况,慢慢你就能总结出什么样的参数组合最稳。这套"跑通—拆开—换素材—记录结果"的流程走一遍,比看十遍讲解都管用。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
二合一支付码生成器 - Python + tkinter 版本
将微信和支付宝收款二维码物理融合为一个二维码
基于github上WeAliPay作者html版的原理,重写核心二维码生成与合并逻辑,PY实现。
"""
 
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import qrcode
from PIL import Image, ImageTk, ImageDraw, ImageFont
import io
import os
import tempfile
 
# ──────────────────────────────────────────────
#  核心引擎:生成 & 合并二维码
# ──────────────────────────────────────────────
class QRCodeEngine:
    """二维码生成与合并引擎"""
 
    @staticmethod
    def generate_qr(url: str, size: int = 400) -> Image.Image:
        """根据 URL 生成二维码 PIL Image"""
        qr = qrcode.QRCode(
            version=None,
            error_correction=qrcode.constants.ERROR_CORRECT_H,
            box_size=10,
            border=4,
        )
        qr.add_data(url)
        qr.make(fit=True)
        img = qr.make_image(fill_color="black", back_color="white")
        img = img.convert("RGB")
        img = img.resize((size, size), Image.LANCZOS)
        return img
 
    @staticmethod
    def merge_qrcodes(
        wechat_url: str,
        alipay_url: str,
        size: int = 400,
        title: str = "扫码支付",
    ) -> Image.Image:
        """
        合并微信和支付宝二维码
 
        原理:
        1. 微信二维码作为底层完整绘制
        2. 支付宝二维码旋转180°后放置到右下角(无定位图案区域)
        3. 这样微信扫码读取主码,支付宝扫码识别右下角旋转后的码
        """
        # 生成两个二维码
        wechat_img = QRCodeEngine.generate_qr(wechat_url, size)
        alipay_img = QRCodeEngine.generate_qr(alipay_url, size)
 
        # 支付宝二维码旋转 180°
        alipay_rotated = alipay_img.rotate(180)
 
        # 右下角区域:取支付宝二维码的右下角 1/4
        half = size // 2
        alipay_piece = alipay_rotated.crop((0, 0, half, half))
 
        # 将支付宝小块贴到微信二维码的右下角
        merged = wechat_img.copy()
        merged.paste(alipay_piece, (half, half))
 
        return merged
 
    @staticmethod
    def create_final_image(
        qr_img: Image.Image,
        title: str = "扫码支付",
    ) -> Image.Image:
        """
        创建最终输出的完整图片,包含:
        - 顶部品牌区域(微信 + 支付宝标识)
        - 中间二维码
        - 底部提示文字
        """
        qr_size = qr_img.width
        canvas_w = qr_size
        top_h = 80
        bottom_h = 60
        qr_display_size = int(qr_size * 0.6)  # 二维码显示区域
        padding = (qr_size - qr_display_size) // 2
 
        total_h = top_h + qr_display_size + padding + bottom_h
        canvas = Image.new("RGB", (canvas_w, total_h), "white")
        draw = ImageDraw.Draw(canvas)
 
        # ── 顶部品牌区域 ──
        draw.rectangle([0, 0, canvas_w, top_h], fill="white")
 
        # 微信图标 (绿色方块)
        icon_size = 60
        icon_gap = 20
        total_icons_w = icon_size * 2 + icon_gap
        start_x = (canvas_w - total_icons_w) // 2
        icon_y = (top_h - icon_size) // 2
 
        draw.rounded_rectangle(
            [start_x, icon_y, start_x + icon_size, icon_y + icon_size],
            radius=12, fill="#07C160"
        )
        draw.rounded_rectangle(
            [start_x + icon_size + icon_gap, icon_y,
             start_x + icon_size * 2 + icon_gap, icon_y + icon_size],
            radius=12, fill="#1677FF"
        )
 
        # 图标文字
        try:
            font_icon = ImageFont.truetype("msyh.ttc", 28)
        except Exception:
            font_icon = ImageFont.load_default()
        draw.text(
            (start_x + icon_size // 2, icon_y + icon_size // 2),
            "W", fill="white", font=font_icon, anchor="mm"
        )
        draw.text(
            (start_x + icon_size + icon_gap + icon_size // 2, icon_y + icon_size // 2),
            "A", fill="white", font=font_icon, anchor="mm"
        )
 
        # ── 二维码区域 ──
        qr_y = top_h
        draw.rectangle(
            [padding, qr_y, padding + qr_display_size, qr_y + qr_display_size],
            fill="white", outline="#EEEEEE", width=1
        )
 
        # 缩放二维码并粘贴
        qr_display = qr_img.resize((qr_display_size, qr_display_size), Image.LANCZOS)
        canvas.paste(qr_display, (padding, qr_y))
 
        # ── 底部提示 ──
        bottom_y = qr_y + qr_display_size + padding
        try:
            font_bottom = ImageFont.truetype("msyh.ttc", 16)
        except Exception:
            font_bottom = ImageFont.load_default()
        draw.text(
            (canvas_w // 2, bottom_y + bottom_h // 2),
            "付款时请核实账户是否正确",
            fill="#333333", font=font_bottom, anchor="mm"
        )
 
        return canvas
 
 
# ──────────────────────────────────────────────
#  tkinter GUI 主界面
# ──────────────────────────────────────────────
class WeAliPayApp:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("微信支付宝二合一支付码生成器")
        self.root.geometry("960x720")
        self.root.minsize(800, 600)
        self.root.configure(bg="#F5F5F5")
 
        self.merged_qr: Image.Image | None = None
        self.final_image: Image.Image | None = None
 
        self._setup_styles()
        self._build_ui()
 
    # ── 样式 ──
    def _setup_styles(self):
        style = ttk.Style()
        style.theme_use("clam")
        style.configure("Primary.TButton", font=("微软雅黑", 11, "bold"),
                        background="#165DFF", foreground="white", borderwidth=0,
                        padding=10)
        style.map("Primary.TButton", background=[("active", "#0040C9")])
        style.configure("Green.TButton", font=("微软雅黑", 10),
                        background="#00B42A", foreground="white", borderwidth=0,
                        padding=8)
        style.map("Green.TButton", background=[("active", "#00871A")])
        style.configure("TLabel", font=("微软雅黑", 10), background="#F5F5F5")
        style.configure("Title.TLabel", font=("微软雅黑", 20, "bold"),
                        background="#F5F5F5", foreground="#1A1A1A")
 
    # ── UI 构建 ──
    def _build_ui(self):
        # 主容器
        main_frame = tk.Frame(self.root, bg="#F5F5F5")
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
 
        # 标题栏
        header = tk.Frame(main_frame, bg="#F5F5F5")
        header.pack(fill=tk.X, pady=(0, 15))
        tk.Label(header, text="🔲 二合一支付码生成器",
                 font=("微软雅黑", 22, "bold"), bg="#F5F5F5", fg="#1A1A1A").pack(side=tk.LEFT)
 
        # 左右分栏
        panes = tk.PanedWindow(main_frame, orient=tk.HORIZONTAL, bg="#E0E0E0", sashwidth=3)
        panes.pack(fill=tk.BOTH, expand=True)
 
        left_panel = tk.Frame(panes, bg="white", bd=0, highlightthickness=0)
        right_panel = tk.Frame(panes, bg="white", bd=0, highlightthickness=0)
 
        panes.add(left_panel, width=420)
        panes.add(right_panel, width=500)
 
        self._build_left_panel(left_panel)
        self._build_right_panel(right_panel)
 
    # ── 左侧:输入区 ──
    def _build_left_panel(self, parent: tk.Frame):
        pad = {"padx": 20, "pady": 5}
 
        # 滚动容器
        canvas = tk.Canvas(parent, bg="white", highlightthickness=0)
        scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=canvas.yview)
        scroll_frame = tk.Frame(canvas, bg="white")
 
        scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
 
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
 
        # 绑定鼠标滚轮
        def _on_mousewheel(event):
            canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        canvas.bind_all("<MouseWheel>", _on_mousewheel)
 
        # ── 收款信息标题 ──
        tk.Label(scroll_frame, text="📋 收款信息", font=("微软雅黑", 14, "bold"),
                 bg="white", fg="#333").pack(anchor=tk.W, **pad)
 
        # 微信收款链接
        tk.Label(scroll_frame, text="微信收款链接", bg="white",
                 fg="#555").pack(anchor=tk.W, padx=20, pady=(15, 2))
        wx_frame = tk.Frame(scroll_frame, bg="white")
        wx_frame.pack(fill=tk.X, padx=20)
        self.wechat_url_var = tk.StringVar()
        tk.Entry(wx_frame, textvariable=self.wechat_url_var, font=("微软雅黑", 10),
                 relief=tk.SOLID, bd=1).pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=4)
        tk.Button(wx_frame, text="上传", bg="#07C160", fg="white", font=("微软雅黑", 9),
                  relief=tk.FLAT, padx=10, cursor="hand2",
                  command=lambda: self._upload_qrcode("wechat")).pack(side=tk.RIGHT, padx=(5, 0))
 
        # 微信提示
        tk.Label(scroll_frame, text="打开微信 → 收付款 → 二维码收款 → 保存图片 → 扫一扫识别 → 复制链接",
                 bg="white", fg="#999", font=("微软雅黑", 8)).pack(anchor=tk.W, padx=20, pady=(2, 10))
 
        # 支付宝收款链接
        tk.Label(scroll_frame, text="支付宝收款链接", bg="white",
                 fg="#555").pack(anchor=tk.W, padx=20, pady=(5, 2))
        ali_frame = tk.Frame(scroll_frame, bg="white")
        ali_frame.pack(fill=tk.X, padx=20)
        self.alipay_url_var = tk.StringVar()
        tk.Entry(ali_frame, textvariable=self.alipay_url_var, font=("微软雅黑", 10),
                 relief=tk.SOLID, bd=1).pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=4)
        tk.Button(ali_frame, text="上传", bg="#1677FF", fg="white", font=("微软雅黑", 9),
                  relief=tk.FLAT, padx=10, cursor="hand2",
                  command=lambda: self._upload_qrcode("alipay")).pack(side=tk.RIGHT, padx=(5, 0))
 
        # 支付宝提示
        tk.Label(scroll_frame, text="打开支付宝 → 收钱 → 保存图片 → 扫一扫识别 → 复制链接",
                 bg="white", fg="#999", font=("微软雅黑", 8)).pack(anchor=tk.W, padx=20, pady=(2, 10))
 
        # ── 分隔线 ──
        ttk.Separator(scroll_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, padx=20, pady=10)
 
        # ── 样式设置 ──
        tk.Label(scroll_frame, text="🎨 样式设置", font=("微软雅黑", 14, "bold"),
                 bg="white", fg="#333").pack(anchor=tk.W, **pad)
 
        # 二维码尺寸
        tk.Label(scroll_frame, text="二维码尺寸", bg="white",
                 fg="#555").pack(anchor=tk.W, padx=20, pady=(10, 2))
        self.size_var = tk.StringVar(value="400")
        size_combo = ttk.Combobox(scroll_frame, textvariable=self.size_var,
                                  values=["300", "400", "500"],
                                  state="readonly", font=("微软雅黑", 10))
        size_combo.pack(fill=tk.X, padx=20, ipady=2)
 
        # 合并模式
        tk.Label(scroll_frame, text="合并模式", bg="white",
                 fg="#555").pack(anchor=tk.W, padx=20, pady=(10, 2))
        self.mode_var = tk.StringVar(value="overlay")
        mode_combo = ttk.Combobox(scroll_frame, textvariable=self.mode_var,
                                  values=["overlay 重叠模式", "split 分割模式", "checkerboard 棋盘模式"],
                                  state="readonly", font=("微软雅黑", 10))
        mode_combo.pack(fill=tk.X, padx=20, ipady=2)
 
        # 自定义标题
        tk.Label(scroll_frame, text="自定义标题", bg="white",
                 fg="#555").pack(anchor=tk.W, padx=20, pady=(10, 2))
        self.title_var = tk.StringVar(value="扫码支付")
        tk.Entry(scroll_frame, textvariable=self.title_var, font=("微软雅黑", 10),
                 relief=tk.SOLID, bd=1).pack(fill=tk.X, padx=20, ipady=4)
 
        # ── 生成按钮 ──
        tk.Button(scroll_frame, text="✨ 生成二合一支付码",
                  bg="#165DFF", fg="white", font=("微软雅黑", 13, "bold"),
                  relief=tk.FLAT, cursor="hand2", pady=12,
                  command=self._generate).pack(fill=tk.X, padx=20, pady=(25, 8))
 
        tk.Label(scroll_frame, text="注意:收款信息仅用于生成,不会被存储",
                 bg="white", fg="#AAA", font=("微软雅黑", 8)).pack(pady=(0, 15))
 
    # ── 右侧:预览区 ──
    def _build_right_panel(self, parent: tk.Frame):
        tk.Label(parent, text="👁 预览与下载", font=("微软雅黑", 14, "bold"),
                 bg="white", fg="#333").pack(anchor=tk.W, padx=20, pady=(15, 10))
 
        # 预览画布
        self.preview_canvas = tk.Canvas(parent, bg="#FAFAFA", highlightthickness=1,
                                        highlightbackground="#E0E0E0")
        self.preview_canvas.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 10))
 
        # 占位提示
        self.preview_canvas.create_text(
            230, 200, text="请输入微信和支付宝收款链接\n然后点击「生成二合一支付码」",
            font=("微软雅黑", 11), fill="#BBB", justify=tk.CENTER
        )
 
        # 下载按钮区域
        btn_frame = tk.Frame(parent, bg="white")
        btn_frame.pack(fill=tk.X, padx=20, pady=(0, 15))
 
        tk.Button(btn_frame, text="📥 下载 PNG",
                  bg="#00B42A", fg="white", font=("微软雅黑", 11),
                  relief=tk.FLAT, cursor="hand2", pady=8,
                  command=lambda: self._download("png")).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        tk.Button(btn_frame, text="📥 下载 JPG",
                  bg="#FF7D00", fg="white", font=("微软雅黑", 11),
                  relief=tk.FLAT, cursor="hand2", pady=8,
                  command=lambda: self._download("jpg")).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
 
    # ── 上传二维码 ──
    def _upload_qrcode(self, target: str):
        path = filedialog.askopenfilename(
            title="选择二维码图片",
            filetypes=[("图片文件", "*.png *.jpg *.jpeg *.bmp"), ("所有文件", "*.*")]
        )
        if not path:
            return
 
        try:
            from PIL import Image as PILImage
            img = PILImage.open(path)
            # 尝试用 pyzbar 解析二维码
            try:
                from pyzbar.pyzbar import decode
                results = decode(img)
                if results:
                    url = results[0].data.decode("utf-8")
                    if target == "wechat":
                        self.wechat_url_var.set(url)
                    else:
                        self.alipay_url_var.set(url)
                    messagebox.showinfo("识别成功", f"已识别链接:\n{url}")
                else:
                    messagebox.showwarning("未识别", "未能从图片中识别出二维码,请手动输入链接。")
            except ImportError:
                messagebox.showinfo("提示",
                    "未安装 pyzbar 库,无法自动解析二维码。\n"
                    "请手动输入链接,或运行: pip install pyzbar")
        except Exception as e:
            messagebox.showerror("错误", f"读取图片失败: {e}")
 
    # ── 生成 ──
    def _generate(self):
        wechat_url = self.wechat_url_var.get().strip()
        alipay_url = self.alipay_url_var.get().strip()
        title = self.title_var.get().strip() or "扫码支付"
 
        if not wechat_url or not alipay_url:
            messagebox.showwarning("提示", "请输入微信和支付宝收款链接")
            return
 
        try:
            size = int(self.size_var.get())
        except ValueError:
            size = 400
 
        mode_raw = self.mode_var.get()
        # 目前以重叠模式为主,其他模式可扩展
        if "split" in mode_raw:
            mode = "split"
        elif "checkerboard" in mode_raw:
            mode = "checkerboard"
        else:
            mode = "overlay"
 
        # 显示加载状态
        self.preview_canvas.delete("all")
        self.preview_canvas.create_text(
            230, 200, text="⏳ 正在生成,请稍候...",
            font=("微软雅黑", 12), fill="#999"
        )
        self.root.update_idletasks()
 
        try:
            if mode == "overlay":
                merged = QRCodeEngine.merge_qrcodes(wechat_url, alipay_url, size, title)
            elif mode == "split":
                merged = self._merge_split(wechat_url, alipay_url, size)
            else:
                merged = self._merge_checkerboard(wechat_url, alipay_url, size)
 
            self.merged_qr = merged
            self.final_image = QRCodeEngine.create_final_image(merged, title)
 
            self._show_preview()
        except Exception as e:
            self.preview_canvas.delete("all")
            self.preview_canvas.create_text(
                230, 200, text=f"❌ 生成失败\n{str(e)[:60]}",
                font=("微软雅黑", 11), fill="#E74C3C", justify=tk.CENTER
            )
 
    def _merge_split(self, wx_url: str, ali_url: str, size: int) -> Image.Image:
        """分割模式:左右各一半"""
        wx_img = QRCodeEngine.generate_qr(wx_url, size)
        ali_img = QRCodeEngine.generate_qr(ali_url, size)
        half = size // 2
        merged = Image.new("RGB", (size, size), "white")
        merged.paste(wx_img.crop((0, 0, half, size)), (0, 0))
        merged.paste(ali_img.crop((half, 0, size, size)), (half, 0))
        return merged
 
    def _merge_checkerboard(self, wx_url: str, ali_url: str, size: int) -> Image.Image:
        """棋盘模式:8x8 格子交替"""
        wx_img = QRCodeEngine.generate_qr(wx_url, size)
        ali_img = QRCodeEngine.generate_qr(ali_url, size)
        merged = Image.new("RGB", (size, size), "white")
        grid = 8
        cell = size // grid
        for r in range(grid):
            for c in range(grid):
                src = wx_img if (r + c) % 2 == 0 else ali_img
                box = (c * cell, r * cell, (c + 1) * cell, (r + 1) * cell)
                merged.paste(src.crop(box), box)
        return merged
 
    # ── 预览显示 ──
    def _show_preview(self):
        if self.final_image is None:
            return
 
        self.preview_canvas.delete("all")
        cw = self.preview_canvas.winfo_width()
        ch = self.preview_canvas.winfo_height()
 
        if cw < 10:
            cw = 480
        if ch < 10:
            ch = 500
 
        # 缩放适应画布
        img = self.final_image.copy()
        iw, ih = img.size
        scale = min((cw - 20) / iw, (ch - 20) / ih, 1.0)
        new_w, new_h = int(iw * scale), int(ih * scale)
        img = img.resize((new_w, new_h), Image.LANCZOS)
 
        self._preview_tk = ImageTk.PhotoImage(img)
        x = (cw - new_w) // 2
        y = (ch - new_h) // 2
        self.preview_canvas.create_image(x, y, anchor=tk.NW, image=self._preview_tk)
 
    # ── 下载 ──
    def _download(self, fmt: str):
        if self.final_image is None:
            messagebox.showwarning("提示", "请先生成二维码")
            return
 
        path = filedialog.asksaveasfilename(
            title=f"保存为 {fmt.upper()}",
            defaultextension=f".{fmt}",
            filetypes=[(f"{fmt.upper()} 文件", f"*.{fmt}")]
        )
        if not path:
            return
 
        try:
            if fmt == "jpg":
                self.final_image.convert("RGB").save(path, "JPEG", quality=95)
            else:
                self.final_image.save(path, "PNG")
            messagebox.showinfo("成功", f"已保存到:\n{path}")
        except Exception as e:
            messagebox.showerror("错误", f"保存失败: {e}")
 
 
# ──────────────────────────────────────────────
#  入口
# ──────────────────────────────────────────────
def main():
    root = tk.Tk()
    app = WeAliPayApp(root)
 
    # 窗口居中
    root.update_idletasks()
    sw = root.winfo_screenwidth()
    sh = root.winfo_screenheight()
    w = root.winfo_width()
    h = root.winfo_height()
    x = (sw - w) // 2
    y = (sh - h) // 2
    root.geometry(f"+{x}+{y}")
 
    root.mainloop()
 
 
if __name__ == "__main__":
    main()

image.png