# dialogs_tkinter.py import os import tkinter as tk from tkinter import ttk, messagebox, simpledialog import logging logger = logging.getLogger(__name__) class BaseDialog: """对话框基类""" def __init__(self, parent, title, width=400, height=300): self.parent = parent self.result = None # 创建顶层窗口 self.dialog = tk.Toplevel(parent) self.dialog.title(title) self.dialog.geometry(f"{width}x{height}") self.dialog.transient(parent) # 设置父窗口 self.dialog.grab_set() # 模态对话框 # 居中显示 self._center_window() # 创建主框架 self.main_frame = ttk.Frame(self.dialog, padding="10") self.main_frame.pack(fill=tk.BOTH, expand=True) def _center_window(self): """将窗口居中显示""" self.dialog.update_idletasks() width = self.dialog.winfo_width() height = self.dialog.winfo_height() x = (self.dialog.winfo_screenwidth() // 2) - (width // 2) y = (self.dialog.winfo_screenheight() // 2) - (height // 2) self.dialog.geometry(f"{width}x{height}+{x}+{y}") def _create_button_box(self): """创建确定/取消按钮""" btn_frame = ttk.Frame(self.main_frame) btn_frame.pack(fill=tk.X, pady=(10, 0)) self.confirm_btn = ttk.Button(btn_frame, text="确定", command=self._on_confirm) self.confirm_btn.pack(side=tk.RIGHT, padx=5) self.cancel_btn = ttk.Button(btn_frame, text="取消", command=self._on_cancel) self.cancel_btn.pack(side=tk.RIGHT, padx=5) # 绑定回车键和 ESC 键 self.dialog.bind('', lambda e: self._on_confirm()) self.dialog.bind('', lambda e: self._on_cancel()) def _on_confirm(self): """确定按钮回调 - 子类重写""" pass def _on_cancel(self): """取消按钮回调""" self.dialog.destroy() def wait_for_result(self): """等待对话框关闭并返回结果""" self.dialog.wait_window() return self.result class CreatePartitionDialog(BaseDialog): """创建分区对话框""" def __init__(self, parent, disk_path="", total_disk_mib=0.0, max_available_mib=0.0): self.disk_path = disk_path self.total_disk_mib = total_disk_mib self.max_available_mib = max_available_mib super().__init__(parent, "创建分区", 450, 280) logger.debug(f"CreatePartitionDialog initialized for {disk_path}. Total MiB: {total_disk_mib}, Max Available MiB: {max_available_mib}") self._setup_ui() self._initialize_state() def _setup_ui(self): """设置 UI""" # 创建内容框架 content_frame = ttk.Frame(self.main_frame) content_frame.pack(fill=tk.BOTH, expand=True) # 磁盘路径 ttk.Label(content_frame, text="磁盘路径:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) ttk.Label(content_frame, text=self.disk_path).grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) # 分区表类型 ttk.Label(content_frame, text="分区表类型:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5) self.part_type_var = tk.StringVar(value="gpt") self.part_type_combo = ttk.Combobox(content_frame, textvariable=self.part_type_var, values=["gpt", "msdos"], state="readonly", width=15) self.part_type_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5) # 使用最大空间复选框 self.use_max_var = tk.BooleanVar(value=True) self.use_max_check = ttk.Checkbutton(content_frame, text="使用最大可用空间", variable=self.use_max_var, command=self._toggle_size_input) self.use_max_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5) # 分区大小 ttk.Label(content_frame, text="分区大小 (GB):").grid(row=3, column=0, sticky=tk.W, padx=5, pady=5) calculated_max_gb = self.max_available_mib / 1024.0 self.max_gb = max(0.01, calculated_max_gb) self.size_var = tk.DoubleVar(value=self.max_gb) self.size_spin = tk.Spinbox(content_frame, from_=0.01, to=self.max_gb, increment=0.01, textvariable=self.size_var, width=12) self.size_spin.grid(row=3, column=1, sticky=tk.W, padx=5, pady=5) logger.debug(f"Size spinbox max set to: {self.max_gb} GB (calculated from {calculated_max_gb} GB)") # 按钮 self._create_button_box() def _initialize_state(self): """初始化状态""" self._toggle_size_input() def _toggle_size_input(self): """切换大小输入框状态""" is_checked = self.use_max_var.get() logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}") if is_checked: logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.") self.size_spin.configure(state=tk.DISABLED) self.size_var.set(self.max_gb) logger.debug(f"[_toggle_size_input] Spinbox value set to max: {self.max_gb} GB.") else: logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.") self.size_spin.configure(state=tk.NORMAL) current_val = self.size_var.get() if current_val >= self.max_gb: self.size_var.set(0.01) logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: 0.01 GB.") else: logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {current_val} GB.") def _on_confirm(self): """确定按钮回调""" size_gb = self.size_var.get() use_max_space = self.use_max_var.get() if not use_max_space and size_gb <= 0: messagebox.showwarning("输入错误", "分区大小必须大于0。") return if not use_max_space and size_gb > self.max_gb: messagebox.showwarning("输入错误", "分区大小不能超过最大可用空间。") return self.result = { 'disk_path': self.disk_path, 'partition_table_type': self.part_type_var.get(), 'size_gb': size_gb, 'total_disk_mib': self.total_disk_mib, 'use_max_space': use_max_space } self.dialog.destroy() def get_partition_info(self): """获取分区信息""" return self.result class MountDialog(BaseDialog): """挂载对话框""" def __init__(self, parent, device_path=""): self.device_path = device_path super().__init__(parent, "挂载分区", 400, 200) self._setup_ui() def _setup_ui(self): """设置 UI""" # 创建内容框架 content_frame = ttk.Frame(self.main_frame) content_frame.pack(fill=tk.BOTH, expand=True) # 设备路径 ttk.Label(content_frame, text="设备路径:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) ttk.Label(content_frame, text=self.device_path).grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) # 挂载点 ttk.Label(content_frame, text="挂载点:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5) self.mount_point_var = tk.StringVar(value=f"/mnt/{os.path.basename(self.device_path)}") self.mount_point_entry = ttk.Entry(content_frame, textvariable=self.mount_point_var, width=30) self.mount_point_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5) # 开机自动挂载复选框 self.add_to_fstab_var = tk.BooleanVar(value=False) self.fstab_check = ttk.Checkbutton(content_frame, text="开机自动挂载 (添加到 /etc/fstab)", variable=self.add_to_fstab_var) self.fstab_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5) # 按钮 self._create_button_box() def _on_confirm(self): """确定按钮回调""" mount_point = self.mount_point_var.get().strip() if not mount_point: messagebox.showwarning("输入错误", "挂载点不能为空。") return if not mount_point.startswith('/'): messagebox.showwarning("输入错误", "挂载点必须是绝对路径 (以 '/' 开头)。") return self.result = { 'mount_point': mount_point, 'add_to_fstab': self.add_to_fstab_var.get() } self.dialog.destroy() def get_mount_info(self): """获取挂载信息""" return self.result class CreateRaidDialog(BaseDialog): """创建 RAID 阵列对话框""" def __init__(self, parent, available_devices=None): self.available_devices = available_devices if available_devices is not None else [] self.selected_devices = [] super().__init__(parent, "创建 RAID 阵列", 450, 450) self._setup_ui() def _setup_ui(self): """设置 UI""" # 创建内容框架 content_frame = ttk.Frame(self.main_frame) content_frame.pack(fill=tk.BOTH, expand=True) # RAID 级别 ttk.Label(content_frame, text="RAID 级别:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.raid_level_var = tk.StringVar(value="raid0") self.raid_level_combo = ttk.Combobox(content_frame, textvariable=self.raid_level_var, values=["raid0", "raid1", "raid5", "raid6", "raid10"], state="readonly", width=12) self.raid_level_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) # Chunk 大小 ttk.Label(content_frame, text="Chunk 大小 (KB):").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5) self.chunk_size_var = tk.IntVar(value=512) self.chunk_size_spin = tk.Spinbox(content_frame, from_=4, to=1024, increment=4, textvariable=self.chunk_size_var, width=10) self.chunk_size_spin.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5) # 设备选择 ttk.Label(content_frame, text="选择设备:").grid(row=2, column=0, sticky=tk.NW, padx=5, pady=5) # 创建设备选择框架 device_frame = ttk.Frame(content_frame) device_frame.grid(row=2, column=1, sticky=tk.W, padx=5, pady=5) # 设备复选框列表 self.device_vars = {} for i, dev in enumerate(self.available_devices): var = tk.BooleanVar(value=False) self.device_vars[dev] = var cb = ttk.Checkbutton(device_frame, text=dev, variable=var) cb.pack(anchor=tk.W) # 按钮 self._create_button_box() def _on_confirm(self): """确定按钮回调""" # 收集选中的设备 self.selected_devices = [dev for dev, var in self.device_vars.items() if var.get()] if not self.selected_devices: messagebox.showwarning("输入错误", "请选择至少一个设备来创建 RAID 阵列。") return raid_level = self.raid_level_var.get() num_devices = len(self.selected_devices) # RAID 级别检查 if raid_level == "raid0" and num_devices < 1: messagebox.showwarning("输入错误", "RAID0 至少需要一个设备。") return elif raid_level == "raid1" and num_devices < 2: messagebox.showwarning("输入错误", "RAID1 至少需要两个设备。") return elif raid_level == "raid5" and num_devices < 3: messagebox.showwarning("输入错误", "RAID5 至少需要三个设备。") return elif raid_level == "raid6" and num_devices < 4: messagebox.showwarning("输入错误", "RAID6 至少需要四个设备。") return elif raid_level == "raid10" and num_devices < 2: messagebox.showwarning("输入错误", "RAID10 至少需要两个设备。") return self.result = { 'devices': self.selected_devices, 'level': raid_level.replace('raid', ''), 'chunk_size': self.chunk_size_var.get() } self.dialog.destroy() def get_raid_info(self): """获取 RAID 信息""" return self.result class CreatePvDialog(BaseDialog): """创建物理卷 (PV) 对话框""" def __init__(self, parent, available_partitions=None): self.available_partitions = available_partitions if available_partitions is not None else [] super().__init__(parent, "创建物理卷 (PV)", 400, 200) self._setup_ui() def _setup_ui(self): """设置 UI""" # 创建内容框架 content_frame = ttk.Frame(self.main_frame) content_frame.pack(fill=tk.BOTH, expand=True) # 设备选择 ttk.Label(content_frame, text="选择设备:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.device_var = tk.StringVar() if self.available_partitions: self.device_var.set(self.available_partitions[0]) self.device_combo = ttk.Combobox(content_frame, textvariable=self.device_var, values=self.available_partitions, state="readonly", width=30) self.device_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) # 按钮 self._create_button_box() def _on_confirm(self): """确定按钮回调""" device_path = self.device_var.get() if not device_path: messagebox.showwarning("输入错误", "请选择一个设备来创建物理卷。") return self.result = {'device_path': device_path} self.dialog.destroy() def get_pv_info(self): """获取 PV 信息""" return self.result class CreateVgDialog(BaseDialog): """创建卷组 (VG) 对话框""" def __init__(self, parent, available_pvs=None): self.available_pvs = available_pvs if available_pvs is not None else [] self.selected_pvs = [] super().__init__(parent, "创建卷组 (VG)", 450, 350) self._setup_ui() def _setup_ui(self): """设置 UI""" # 创建内容框架 content_frame = ttk.Frame(self.main_frame) content_frame.pack(fill=tk.BOTH, expand=True) # 卷组名称 ttk.Label(content_frame, text="卷组名称:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.vg_name_var = tk.StringVar() self.vg_name_entry = ttk.Entry(content_frame, textvariable=self.vg_name_var, width=25) self.vg_name_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) # 物理卷选择 ttk.Label(content_frame, text="选择物理卷:").grid(row=1, column=0, sticky=tk.NW, padx=5, pady=5) pv_frame = ttk.Frame(content_frame) pv_frame.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5) self.pv_vars = {} for pv in self.available_pvs: var = tk.BooleanVar(value=False) self.pv_vars[pv] = var cb = ttk.Checkbutton(pv_frame, text=pv, variable=var) cb.pack(anchor=tk.W) # 按钮 self._create_button_box() def _on_confirm(self): """确定按钮回调""" vg_name = self.vg_name_var.get().strip() self.selected_pvs = [pv for pv, var in self.pv_vars.items() if var.get()] if not vg_name: messagebox.showwarning("输入错误", "卷组名称不能为空。") return if not self.selected_pvs: messagebox.showwarning("输入错误", "请选择至少一个物理卷来创建卷组。") return self.result = { 'vg_name': vg_name, 'pvs': self.selected_pvs } self.dialog.destroy() def get_vg_info(self): """获取 VG 信息""" return self.result class CreateLvDialog(BaseDialog): """创建逻辑卷 (LV) 对话框""" def __init__(self, parent, available_vgs=None, vg_sizes=None): self.available_vgs = available_vgs if available_vgs is not None else [] self.vg_sizes = vg_sizes if vg_sizes is not None else {} super().__init__(parent, "创建逻辑卷", 450, 300) logger.debug(f"CreateLvDialog initialized. Available VGs: {self.available_vgs}, VG Sizes: {self.vg_sizes}") self._setup_ui() self._initialize_state() def _setup_ui(self): """设置 UI""" # 逻辑卷名称 # 创建内容框架 content_frame = ttk.Frame(self.main_frame) content_frame.pack(fill=tk.BOTH, expand=True) ttk.Label(content_frame, text="逻辑卷名称:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.lv_name_var = tk.StringVar() self.lv_name_entry = ttk.Entry(content_frame, textvariable=self.lv_name_var, width=25) self.lv_name_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) # 卷组选择 ttk.Label(content_frame, text="选择卷组:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5) self.vg_var = tk.StringVar() if self.available_vgs: self.vg_var.set(self.available_vgs[0]) self.vg_combo = ttk.Combobox(content_frame, textvariable=self.vg_var, values=self.available_vgs, state="readonly", width=25) self.vg_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5) self.vg_combo.bind('<>', lambda e: self._update_size_options()) # 使用最大空间复选框 self.use_max_var = tk.BooleanVar(value=True) self.use_max_check = ttk.Checkbutton(content_frame, text="使用最大可用空间", variable=self.use_max_var, command=self._toggle_size_input) self.use_max_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5) # 逻辑卷大小 ttk.Label(content_frame, text="大小 (GB):").grid(row=3, column=0, sticky=tk.W, padx=5, pady=5) self.size_var = tk.DoubleVar(value=1.0) self.size_spin = tk.Spinbox(content_frame, from_=0.01, to=100000.0, increment=0.01, textvariable=self.size_var, width=12) self.size_spin.grid(row=3, column=1, sticky=tk.W, padx=5, pady=5) # 按钮 self._create_button_box() def _initialize_state(self): """初始化状态""" self._update_size_options() def _update_size_options(self): """根据选中的卷组更新大小选项""" selected_vg = self.vg_var.get() max_size_gb = self.vg_sizes.get(selected_vg, 0.0) logger.debug(f"[_update_size_options] Selected VG: {selected_vg}, Max available GB: {max_size_gb}") # 确保最大值至少大于等于最小值 effective_max = max(0.01, max_size_gb) self.size_spin.configure(to=effective_max) self.max_gb = effective_max logger.debug(f"[_update_size_options] Spinbox max set to: {effective_max} GB.") # 应用复选框状态 self._toggle_size_input() logger.debug("[_update_size_options] Completed.") def _toggle_size_input(self): """切换大小输入框状态""" is_checked = self.use_max_var.get() logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}") if is_checked: logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.") self.size_spin.configure(state=tk.DISABLED) self.size_var.set(self.max_gb) logger.debug(f"[_toggle_size_input] Spinbox value set to max: {self.max_gb} GB.") else: logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.") self.size_spin.configure(state=tk.NORMAL) current_val = self.size_var.get() if current_val >= self.max_gb: self.size_var.set(0.01) logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: 0.01 GB.") else: logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {current_val} GB.") logger.debug("[_toggle_size_input] Completed.") def _on_confirm(self): """确定按钮回调""" lv_name = self.lv_name_var.get().strip() vg_name = self.vg_var.get() size_gb = self.size_var.get() use_max_space = self.use_max_var.get() if not lv_name: messagebox.showwarning("输入错误", "逻辑卷名称不能为空。") return if not vg_name: messagebox.showwarning("输入错误", "请选择一个卷组。") return if not use_max_space: if size_gb <= 0: messagebox.showwarning("输入错误", "逻辑卷大小必须大于0。") return if size_gb > self.max_gb: messagebox.showwarning("输入错误", "逻辑卷大小不能超过卷组的可用空间。") return self.result = { 'lv_name': lv_name, 'vg_name': vg_name, 'size_gb': size_gb, 'use_max_space': use_max_space } self.dialog.destroy() def get_lv_info(self): """获取 LV 信息""" return self.result