importtkinterastkfromtkinterimportfiledialogimportcustomtkinterasctkfromPILimportImage,ImageTkimportplatform# <span style="color: red;">【关键配置】解除 Pillow 的大图像素限制</span>Image.MAX_IMAGE_PIXELS=Nonectk.set_appearance_mode("Dark")classViewportImageViewer(ctk.CTk):def__init__(self):super().__init__()self.title("无限大图查看器 (视口渲染 + 坐标显示)")self.geometry("1100x750")# --- 核心数据 ---self.src_image=None# 原图对象 (Lazy Load)self.current_scale=1.0# 缩放倍率self.img_pos_x=0# 图片在画布上的左上角 Xself.img_pos_y=0# 图片在画布上的左上角 Y# 交互状态self.last_mouse_x=0self.last_mouse_y=0self.render_job=None# 防抖任务定时器# --- UI 布局 ---self.grid_columnconfigure(1,weight=1)self.grid_rowconfigure(0,weight=1)# 1. 左侧控制栏self.sidebar=ctk.CTkFrame(self,width=200,corner_radius=0)self.sidebar.grid(row=0,column=0,sticky="nsew")ctk.CTkLabel(self.sidebar,text="Ultra Viewer",font=("Arial",20,"bold")).pack(pady=30)ctk.CTkButton(self.sidebar,text="📂 打开超大图",command=self.open_image).pack(pady=10,padx=20)self.info_label=ctk.CTkLabel(self.sidebar,text="等待加载...",text_color="gray")self.info_label.pack(pady=10)# --- 新增:坐标显示区域 ---self.coord_card=ctk.CTkFrame(self.sidebar,fg_color="gray20",corner_radius=10)self.coord_card.pack(pady=30,padx=20,fill="x")ctk.CTkLabel(self.coord_card,text="X / Y 坐标",font=("Arial",12)).pack(pady=5)self.lbl_coord=ctk.CTkLabel(self.coord_card,text="- , -",font=("Arial",18,"bold"),text_color="#3B8ED0")self.lbl_coord.pack(pady=(0,15))# 调试信息 (可选)self.debug_label=ctk.CTkLabel(self.sidebar,text="",font=("Consolas",10),text_color="gray50")self.debug_label.pack(side="bottom",pady=20,anchor="w",padx=10)# 2. 右侧画布self.canvas=tk.Canvas(self,bg="#2b2b2b",highlightthickness=0)self.canvas.grid(row=0,column=1,sticky="nsew")# --- 事件绑定 ---# 拖拽相关self.canvas.bind("<ButtonPress-1>",self.on_mouse_down)self.canvas.bind("<B1-Motion>",self.on_mouse_drag)# 滚轮缩放ifplatform.system()=="Linux":self.canvas.bind("<Button-4>",lambdae:self.on_zoom(e,1.1))self.canvas.bind("<Button-5>",lambdae:self.on_zoom(e,0.9))else:self.canvas.bind("<MouseWheel>",self.on_wheel)# 窗口重绘self.canvas.bind("<Configure>",lambdae:self.request_render())# --- 新增:鼠标移动监听 (用于更新坐标) ---self.canvas.bind("<Motion>",self.show_coords)defopen_image(self):file_path=filedialog.askopenfilename()ifnotfile_path:returntry:# Lazy Load: 只读头信息,不读像素self.src_image=Image.open(file_path)# 初始化:适应屏幕win_w=self.canvas.winfo_width()win_h=self.canvas.winfo_height()img_w,img_h=self.src_image.size self.current_scale=min(win_w/img_w,win_h/img_h)*0.9# 居中计算disp_w=img_w*self.current_scale disp_h=img_h*self.current_scale self.img_pos_x=(win_w-disp_w)/2self.img_pos_y=(win_h-disp_h)/2self.info_label.configure(text=f"尺寸:{img_w}x{img_h}\n格式:{self.src_image.format}")self.request_render()exceptExceptionase:print(f"Error:{e}")defshow_coords(self,event):"""新增:实时计算鼠标下的真实图片坐标"""ifnotself.src_image:return# 1. 计算相对于图片左上角的屏幕像素距离# 公式: 鼠标屏幕位置 - 图片左上角屏幕位置screen_rel_x=event.x-self.img_pos_x screen_rel_y=event.y-self.img_pos_y# 2. 换算回原图尺寸# 公式: 屏幕距离 / 缩放倍率real_x=int(screen_rel_x/self.current_scale)real_y=int(screen_rel_y/self.current_scale)# 3. 边界检查 (防止显示负数或超出图片范围)if0<=real_x<self.src_image.widthand0<=real_y<self.src_image.height:self.lbl_coord.configure(text=f"{real_x},{real_y}",text_color="#3B8ED0")else:self.lbl_coord.configure(text="越界",text_color="red")defon_mouse_down(self,event):self.last_mouse_x=event.x self.last_mouse_y=event.ydefon_mouse_drag(self,event):ifnotself.src_image:returndx=event.x-self.last_mouse_x dy=event.y-self.last_mouse_y self.img_pos_x+=dx self.img_pos_y+=dy self.last_mouse_x=event.x self.last_mouse_y=event.y# 拖拽时只移动画布元素,不重绘图片内容 (高性能)self.canvas.move("img_tag",dx,dy)# 拖拽时也要更新坐标self.show_coords(event)self.debounce_render()defon_wheel(self,event):factor=1.1ifevent.delta>0else0.9self.on_zoom(event,factor)defon_zoom(self,event,factor):ifnotself.src_image:returnmouse_x=event.x mouse_y=event.y# 记录鼠标在图片内部的相对比例 (0.0~1.0)rel_x=(mouse_x-self.img_pos_x)/(self.src_image.width*self.current_scale)rel_y=(mouse_y-self.img_pos_y)/(self.src_image.height*self.current_scale)# 更新缩放self.current_scale*=factor# 修正位置,保持鼠标下的点不动new_w=self.src_image.width*self.current_scale new_h=self.src_image.height*self.current_scale self.img_pos_x=mouse_x-(rel_x*new_w)self.img_pos_y=mouse_y-(rel_y*new_h)self.request_render()# 缩放后立即更新坐标显示self.show_coords(event)defdebounce_render(self):ifself.render_job:self.after_cancel(self.render_job)self.render_job=self.after(50,self.request_render)defrequest_render(self):ifnotself.src_image:returnwin_w=self.canvas.winfo_width()win_h=self.canvas.winfo_height()# --- 视口裁切算法 ---# 计算可视区域对应的原图坐标范围left=-self.img_pos_x/self.current_scale top=-self.img_pos_y/self.current_scale right=(win_w-self.img_pos_x)/self.current_scale bottom=(win_h-self.img_pos_y)/self.current_scale crop_left=max(0,int(left))crop_top=max(0,int(top))crop_right=min(self.src_image.width,int(right)+1)crop_bottom=min(self.src_image.height,int(bottom)+1)ifcrop_right<=crop_leftorcrop_bottom<=crop_top:self.canvas.delete("img_tag")returntry:# 1. 从硬盘裁切 (Crop)tile=self.src_image.crop((crop_left,crop_top,crop_right,crop_bottom))# 2. 缩放到屏幕显示尺寸 (Resize)display_w=int((crop_right-crop_left)*self.current_scale)display_h=int((crop_bottom-crop_top)*self.current_scale)ifdisplay_w>0anddisplay_h>0:# 使用 Nearest 模式以获得最快速度 (大图浏览通常不需要插值平滑)tile=tile.resize((display_w,display_h),Image.Resampling.NEAREST)self.tk_image=ImageTk.PhotoImage(tile)# 3. 放置到画布canvas_x=self.img_pos_x+crop_left*self.current_scale canvas_y=self.img_pos_y+crop_top*self.current_scale self.canvas.delete("img_tag")self.canvas.create_image(canvas_x,canvas_y,anchor="nw",image=self.tk_image,tags="img_tag")self.debug_label.configure(text=f"View:{crop_left}:{crop_top}->{crop_right}:{crop_bottom}")exceptExceptionase:print(f"Render Error:{e}")if__name__=="__main__":app=ViewportImageViewer()app.mainloop()只要鼠标放在图片区域内就能显示坐标,图片支持无限制放大缩小,移动位置,流畅不卡顿