UI patterns 翻译 : 幻灯片

本文探讨了幻灯片模式在展示图片新闻时的使用场景与注意事项,包括节省屏幕空间、吸引用户注意力、避免过度使用动画,以及如何通过按钮、导航和完整图像或标题标签提升用户体验。

对UI patterns.com 的一篇文章翻译,幻灯片模式

 

 

Slideshow

Problem summary

The user needs to skim through stories without scrolling or any other unnecessary mouse movements.

Example

 

Usage

(1) Use when you need to tease multiple stories, but want to save screen real estate

(2) Use when you need to direct users’ attention toward stories that you have highlighted

(3) Use when you want to allow users to skim several stories without scrolling, doing any other mouse movements, or use any other input devise.

(4) Do not use if you want users to view all stories at the same time.

(5) Beware of over-usage and combination with other animations, which can lead to making a website seem too busy and attention-demanding.

Solution

A slideshow shows several stories with images, one at a time. After a specific set time interval one story is replaced by another – often with an animated transition.

Transitions

Transitions between images are most often a sliding effect although a simple fade is also a popular choice. The most important design choice when it comes to transitions is to make it seem natural. Animations should never be used for showing off; only to support the usability and understandability of UI.

Numbers, bullets, or thumbnails

Use numbers, bullets, squares, or thumbnails to represent all the images in the slideshow. These provide a way of letting the user have a sense of where he or she is in the slideshow and help set expectations as to how many stories are left and available.

Use numbers if it’s important to let the user now exactly how many stories a slideshow has. Use bullets if it doesn’t matter, and thumbnails if you want to inspire the user to jump past the sequential order of stories that you’ve chosen beforehand.

Focus attention

Slideshows steal attention! Especially if they are combined with animated transitions. Put slideshows together with blinking advertisement and other bright, animated or otherwise attention-stealing elements on the page and you have mayhem. If more than one element screams for attention, the user will get lost. If you have multiple elements that scream for attention other than the slideshow, the slideshow will only help diffuse users’ attention instead of focusing it.

Consider whether your slideshow is going to represent the main and most important stories of your site – if it doesn’t, then leave out the slideshow. A slideshow directs attention towards itself. Don’t overdo it.

Buttons and good callout texts

Increase the effectiveness of your slideshow by adding buttons for each story that calls out for attention. Buttons help users know what to click. However, be careful not to fall in the common trap of just labeling your button with “Read more”, unless that is really the only action the user can do by clicking on that button. Texts like “Support”, “Donate”, “Buy”, and “Watch video” are much more effective in getting users to click and set expectations of what they will get.

Navigation

Common navigation elements include:

(1)  Previous and next buttons

(2)  Bullets, numbers, or thumbnails

(3)  Callout buttons

In order not to present the user with too many options at first, consider hiding navigational elements (such as “previous” and “next” buttons) until users hover the slideshow image. Too many options at first can confuse users and make them go away before they even got started. Reveal their options as their interest is sparked.

Full image or tabs with title

Slideshows seem to go one of two ways:

(1)   Either the image of the story fills the entire slideshow. The current story is represented by a big image that acts as a background with text on top. This version provide the biggest sensory experience as it focuses on as large images as possible.

(2)   Or the stories in the slideshow is listed either horizontally or vertically on the side or below or on top of the image. This version focus on conveying titles and text more than a visual sensory experience. Use this type if the title of a story is so important that the user can’t wait till that one story is up.

Rationale

Slideshows highlight several different stories on the same screen real estate. They allow users to quickly skim through stories without scrolling, moving the mouse or in any other way use navigation options. Users at the same time save attention as they don’t have to concentrate on navigation but at the same time calls for attention every time the slideshow switches to a new image. Slideshows focus users’ attention sharply on the content instead of interacting with the browser.

 

 

 

 

 

 

 

 

 

 

 

 

译文:

 

幻灯片

问题总结

 

用户在浏览图片新闻的时候不用滚动鼠标或任何其他不必要的鼠标操作。

例子

 

 

 

使用场景

(1)  当你想展示多个图片新闻,但又想节省屏幕空间时的时候使用

(2)  当你想吸引用户眼球,让用户第一时间浏览到头条新闻的时候使用

(3)  当你想让用户在浏览图片新闻的时候不用鼠标操作或很少的鼠标操作的时候使用

(4)  当你想让用户在同一时间看到所以图片的时候不要使用

(5)  谨防过度使用幻灯片或者其与其他动画的组合。过度使用幻灯片或者其与其他动画的组合,这可能会导致制作一个网站花费很多精力在这些特效上

解决方案

图片新闻会在一段时间轮流展示。在一定的时间间隔,一个新闻会被另一个新闻来替代,经常以动画效果的形式来切换。

切换

在图像之间的切换最流行也最简单的方式是用滑动来实现。最重要的设计原则是让图片切换的时候非常自然。动画不应仅用于展示,并且还致力于优秀的UI体验。

数字,小圆点,或者缩略图

用数字,小圆点,或者缩略图表示当前幻灯片里所有的图片的信息,可以让用户了解到预期有几个图片。

用数字可以让用户了解当前幻灯片里所有的图片的个数。如果这不太重要的话也可以用小圆点。用缩略图的话可以由用户绕过我们设定的顺序自由的选择他们想要看的那一张图片。

集中注意力

幻灯片可以潜移默化的让用户集中注意力,特别是在你加上切换动画的话。如果是幻灯片加上闪烁的广告又加上其他动画的时候就会让用户感到困惑,一致感觉你的网站不好用,用户则有可能流失。如果你有比幻灯片更重要的信息要展示,幻灯片只会起反作用让用户分散注意力。

要考虑网站整体最主要的展示信息是什么,如果不是幻灯片里的信息,那就不要用幻灯片。幻灯片会吸引别人的眼球,但也不要太过:)。

按钮和良好的标注文字

在你的幻灯片上添加在增加效果,按钮可以让用户知道可以点击什么。不过,也要警惕这些按钮,除非用户点击按钮可以唯一的可执行操作。如果用户有需求的话,可以加一点一下按钮,比如:像“支持”,“捐赠”,“买入”和“观看视频”。

导航

常见的导航元素包括:

(1) 前一个和后一个按钮

(2) 小圆圈,数字或缩略图

(3) 调出按钮

为了不给用户太多选择,建议如果用户鼠标移出幻灯片或者进入的时候就隐藏导航元素(比如上一个下一个),太多的选项也可能让永恒影院感到不舒服而离开。适当的导航可以激发他们的兴趣。

完整的图像或标题标签

有一下两种方式:

(1) 只有图片填充到幻灯片。当前的新闻是由一个大图片作为背景。这个方式提供最大的感官体验,因为它集中在尽可能大的图片。

(2) 在图片下面加上标题。水平尺或垂直的在图像的顶部或下方。这个方式重点传达标题和文本的视觉感官体验。使用这种类型可以很好的给用户介绍图片的主要内容让用户判断是否进入浏览。

基本原理

幻灯片突出了不同的故事在同一屏幕。大幅幻灯片将用户的注意力集中于重要内容。 在同一画面真实几种不同的故事房地产(房地产)。它们允许用户在不滚动鼠标的情况下快速阅读。用户自己可以移动鼠标或以任何其他方式使用导航选项。同时用户节省关注,因为他们不必自己集中注意力找网站提供的重要,只需注意幻灯片的自动切换。幻灯片主要功能是吸引用户的注意力,使得用户不需要与浏览器进行交互。

 

 

 

 

这是我的日志:[Running] python -u "e:\system\Desktop\项目所需文件\工具\文件搜索工具\File Content Search Tool.py" libpng warning: iCCP: known incorrect sRGB profile libpng warning: iCCP: known incorrect sRGB profile Exception in thread Thread-1 (perform_search): Traceback (most recent call last): File "e:\system\Desktop\\u9879�ڏ�������\�H��\�����r���H��\File Content Search Tool.py", line 437, in search_in_office_file wb = load_workbook(filepath, read_only=True, data_only=True) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\cheny9210\AppData\Local\Programs\Python\Python312\Lib\site-packages\openpyxl\reader\excel.py", line 346, in load_workbook reader = ExcelReader(filename, read_only, keep_vba, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\cheny9210\AppData\Local\Programs\Python\Python312\Lib\site-packages\openpyxl\reader\excel.py", line 123, in __init__ self.archive = _validate_archive(fn) ^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\cheny9210\AppData\Local\Programs\Python\Python312\Lib\site-packages\openpyxl\reader\excel.py", line 95, in _validate_archive archive = ZipFile(filename, 'r') ^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\cheny9210\AppData\Local\Programs\Python\Python312\Lib\zipfile\__init__.py", line 1331, in __init__ self.fp = io.open(file, filemode) ^^^^^^^^^^^^^^^^^^^^^^^ PermissionError: [Errno 13] Permission denied: 'E:/SVN/DH_D82D_D83D/trunk/10COMMUNICATION/1001Minutes/12.DRBFM\\~$D82DD83D_CANDI�Ή�_DRBFM.xlsm' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "e:\system\Desktop\\u9879�ڏ�������\�H��\�����r���H��\File Content Search Tool.py", line 330, in perform_search matches = self.search_in_office_file(file_path, pattern) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "e:\system\Desktop\\u9879�ڏ�������\�H��\�����r���H��\File Content Search Tool.py", line 534, in search_in_office_file print(f"\u5904��Office������\u8d25 {filepath}: {str(e)}") UnicodeEncodeError: 'cp932' codec can't encode character '\u5904' in position 0: illegal multibyte sequence During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\cheny9210\AppData\Local\Programs\Python\Python312\Lib\threading.py", line 1075, in _bootstrap_inner self.run() File "C:\Users\cheny9210\AppData\Local\Programs\Python\Python312\Lib\threading.py", line 1012, in run self._target(*self._args, **self._kwargs) File "e:\system\Desktop\\u9879�ڏ�������\�H��\�����r���H��\File Content Search Tool.py", line 359, in perform_search print(error_info) UnicodeEncodeError: 'cp932' codec can't encode character '\u9519' in position 2: illegal multibyte sequence 这是我的代码:import os import re import sys import time import threading import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext from tkinter.font import Font import fnmatch import subprocess import shutil import docx from openpyxl import load_workbook import PyPDF2 import zipfile import chardet import xlrd # 添加对旧版Excel的支持 class FileSearchApp: def __init__(self, master): self.master = master master.title("高级文件搜索工具") master.geometry("1200x800") master.minsize(900, 650) # 设置现代主题 self.style = ttk.Style() self.style.theme_use("vista" if sys.platform == "win32" else "aqua") # 创建主框架 main_frame = ttk.Frame(master, padding=10) main_frame.pack(fill=tk.BOTH, expand=True) # 创建左侧搜索面板 search_frame = ttk.LabelFrame(main_frame, text="搜索选项", padding=10) search_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10), pady=5) # 使用网格布局管理器 row = 0 ttk.Label(search_frame, text="搜索目录:").grid(row=row, column=0, sticky="w", pady=5) self.dir_entry = ttk.Entry(search_frame, width=40) self.dir_entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") self.dir_entry.insert(0, os.getcwd()) ttk.Button(search_frame, text="浏览...", command=self.browse_directory).grid(row=row, column=2, padx=5, pady=5) row += 1 ttk.Label(search_frame, text="关键词:").grid(row=row, column=0, sticky="w", pady=5) self.keyword_entry = ttk.Entry(search_frame, width=40) self.keyword_entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") row += 1 ttk.Label(search_frame, text="文件过滤:").grid(row=row, column=0, sticky="w", pady=5) self.filter_entry = ttk.Entry(search_frame, width=40) self.filter_entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") self.filter_entry.insert(0, "*") row += 1 # 添加分隔线 ttk.Separator(search_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=3, sticky="ew", pady=10) row += 1 # 搜索选项 options_frame = ttk.Frame(search_frame) options_frame.grid(row=row, column=0, columnspan=3, sticky="we", padx=5, pady=5) # 使用网格布局替代pack布局,更紧凑 self.case_var = tk.BooleanVar(value=False) ttk.Checkbutton(options_frame, text="忽略大小写", variable=self.case_var).grid(row=0, column=0, sticky="w", padx=(0, 10)) self.regex_var = tk.BooleanVar(value=False) ttk.Checkbutton(options_frame, text="正则表达式", variable=self.regex_var).grid(row=0, column=1, sticky="w", padx=(0, 10)) self.binary_var = tk.BooleanVar(value=False) self.binary_check = ttk.Checkbutton(options_frame, text="包含二进制", variable=self.binary_var) self.binary_check.grid(row=0, column=2, sticky="w") # 添加文件大小限制选项 self.limit_var = tk.BooleanVar(value=True) ttk.Checkbutton(options_frame, text="限制大小(100MB)", variable=self.limit_var).grid(row=0, column=3, sticky="w", padx=(10, 0)) row += 1 # 添加分隔线 ttk.Separator(search_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=3, sticky="ew", pady=10) row += 1 # 搜索按钮 button_frame = ttk.Frame(search_frame) button_frame.grid(row=row, column=0, columnspan=3, pady=10) self.search_button = ttk.Button(button_frame, text="开始搜索", command=self.start_search) self.search_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button(button_frame, text="停止搜索", command=self.stop_search, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=5) self.export_button = ttk.Button(button_frame, text="导出结果", command=self.export_results) self.export_button.pack(side=tk.LEFT, padx=5) row += 1 # 添加分隔线 ttk.Separator(search_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=3, sticky="ew", pady=10) row += 1 # 状态栏 status_frame = ttk.Frame(search_frame) status_frame.grid(row=row, column=0, columnspan=3, sticky="we", pady=5) # 状态标签(左对齐) self.status_label = ttk.Label(status_frame, text="就绪", font=("Arial", 9)) self.status_label.pack(side=tk.LEFT, anchor='w') # 进度条(中间,可伸缩) self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar( status_frame, variable=self.progress_var, length=200, mode='determinate' ) self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10) # 结果统计(右对齐) self.stats_label = ttk.Label(status_frame, text="", font=("Arial", 9)) self.stats_label.pack(side=tk.RIGHT) # 创建结果面板 results_frame = ttk.LabelFrame(main_frame, text="搜索结果", padding=10) results_frame.pack(fill=tk.BOTH, expand=True, padx=(5, 0), pady=5) # 分割窗格 paned_window = ttk.PanedWindow(results_frame, orient=tk.HORIZONTAL) paned_window.pack(fill=tk.BOTH, expand=True) # 左侧文件列表 file_list_frame = ttk.Frame(paned_window) paned_window.add(file_list_frame, weight=1) # 使用Treeview替代Listbox columns = ("filename", "path") self.file_tree = ttk.Treeview(file_list_frame, columns=columns, show="headings", selectmode="browse") # 设置列标题 self.file_tree.heading("filename", text="文件名") self.file_tree.heading("path", text="路径") # 设置列宽 self.file_tree.column("filename", width=200, anchor="w") self.file_tree.column("path", width=300, anchor="w") self.file_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.file_tree.bind('<<TreeviewSelect>>', self.show_file_content) self.file_tree.bind('<Double-1>', self.open_selected_file) file_scroll = ttk.Scrollbar(file_list_frame, command=self.file_tree.yview) file_scroll.pack(side=tk.RIGHT, fill=tk.Y) self.file_tree.config(yscrollcommand=file_scroll.set) # 右键菜单 self.file_menu = tk.Menu(self.master, tearoff=0) self.file_menu.add_command(label="打开文件", command=self.open_selected_file) self.file_menu.add_command(label="打开文件位置", command=self.open_file_location) self.file_tree.bind("<Button-3>", self.show_file_context_menu) # 右侧文件内容预览 content_frame = ttk.Frame(paned_window) paned_window.add(content_frame, weight=2) self.content_text = scrolledtext.ScrolledText( content_frame, wrap=tk.WORD, font=("Consolas", 10), padx=5, pady=5 ) self.content_text.pack(fill=tk.BOTH, expand=True) # 文本区域右键菜单 text_menu = tk.Menu(self.master, tearoff=0) text_menu.add_command(label="复制", command=self.copy_selected_text) self.content_text.bind("<Button-3>", lambda e: text_menu.tk_popup(e.x_root, e.y_root)) # 高亮标签 self.content_text.tag_configure("match", background="yellow") self.content_text.tag_configure("linenum", foreground="blue") self.content_text.tag_configure("header", foreground="darkgreen", font=("Arial", 10, "bold")) self.content_text.tag_configure("warning", foreground="red", font=("Arial", 10, "italic")) # 初始化变量 self.results = {} self.all_files = [] self.file_paths = [] self.stop_requested = False self.search_thread = None def browse_directory(self): directory = filedialog.askdirectory(title="选择搜索目录") if directory: self.dir_entry.delete(0, tk.END) self.dir_entry.insert(0, directory) def start_search(self): # 重置状态 self.progress_var.set(0) self.stop_requested = False self.results = {} self.all_files = [] self.file_paths = [] self.file_tree.delete(*self.file_tree.get_children()) self.content_text.delete(1.0, tk.END) self.status_label.config(text="正在搜索...") self.search_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.stats_label.config(text="") # 获取搜索参数 directory = self.dir_entry.get().strip() keyword = self.keyword_entry.get().strip() file_filter = self.filter_entry.get().strip() # 验证输入 if not directory or not os.path.isdir(directory): messagebox.showerror("错误", "请选择有效的搜索目录") self.search_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return if not keyword: messagebox.showerror("错误", "请输入搜索关键词") self.search_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return # 解析文件过滤器 if file_filter == "": filter_patterns = ['*'] else: separators = [';', '|', ' ', ','] for sep in separators: if sep in file_filter: filter_patterns = [pat.strip() for pat in file_filter.split(sep)] break else: filter_patterns = [file_filter] # 编译搜索模式 flags = re.IGNORECASE if self.case_var.get() else 0 try: if self.regex_var.get(): pattern = re.compile(keyword, flags) else: escaped_keyword = re.escape(keyword) pattern = re.compile(escaped_keyword, flags) except re.error as e: messagebox.showerror("正则表达式错误", f"无效的正则表达式: {str(e)}") self.search_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return # 在后台线程中执行搜索 self.search_thread = threading.Thread( target=self.perform_search, args=(directory, filter_patterns, pattern), daemon=True ) self.search_thread.start() def perform_search(self, directory, filter_patterns, pattern): """在后台线程中执行文件搜索""" try: # 收集所有匹配的文件 all_files = [] for root, _, files in os.walk(directory): if self.stop_requested: self.master.after(0, lambda: self.status_label.config(text="搜索已取消")) return for file in files: file_path = os.path.join(root, file) # 检查文件大小限制(避免处理超大文件) try: file_size = os.path.getsize(file_path) if file_size > 100 * 1024 * 1024: # 100MB continue except: continue # 检查是否符合任一过滤模式 if any(fnmatch.fnmatch(file, pat) for pat in filter_patterns): all_files.append(file_path) self.all_files = all_files total_files = len(all_files) # 初始化进度条 self.master.after(0, lambda: self.progress_bar.config(maximum=total_files)) self.master.after(0, lambda: self.stats_label.config(text=f"扫描到 {total_files} 个文件")) # 搜索每个文件 self.results = {} processed = 0 matches_found = 0 for file_path in self.all_files: if self.stop_requested: break processed += 1 # 更新进度条(安全方式) self.master.after(0, lambda v=processed: self.progress_var.set(v)) if processed % 10 == 0: # 每处理10个文件更新一次进度 self.master.after(0, lambda p=processed, t=total_files: self.stats_label.config(text=f"处理中: {p}/{t} 文件 ({round(p/t*100,1)}%)")) # 忽略二进制文件(除非用户选择包含) if not self.binary_var.get() and self.is_binary(file_path): continue # 获取文件扩展名 _, ext = os.path.splitext(file_path) ext_lower = ext.lower() # 处理Office文档 if ext_lower in ['.docx', '.xlsx', '.xls', '.xlsm', '.pptx', '.pdf', '.doc']: matches = self.search_in_office_file(file_path, pattern) # 处理压缩文件 elif ext_lower in ['.zip', '.rar', '.7z', '.tar', '.gz']: matches = self.search_in_archive(file_path, pattern) # 处理文本文件 else: matches = self.search_in_text_file(file_path, pattern) if matches: self.results[file_path] = matches matches_found += len(matches) # 在UI线程中添加文件到列表 filename = os.path.basename(file_path) self.master.after(0, lambda fp=file_path, fn=filename: self.file_tree.insert("", "end", values=(fn, fp))) # 更新完成状态 if self.stop_requested: status_text = f"搜索已取消 - 找到 {len(self.results)} 个文件, {matches_found} 个匹配项" else: status_text = f"搜索完成 - 找到 {len(self.results)} 个文件, {matches_found} 个匹配项" self.master.after(0, lambda: self.status_label.config(text=status_text)) self.master.after(0, lambda: self.stats_label.config(text=f"已处理 {processed}/{total_files} 文件")) self.master.after(0, lambda: self.progress_var.set(total_files)) except Exception as e: # 记录详细错误日志 error_info = f"搜索错误: {type(e).__name__} - {str(e)}" print(error_info) with open("search_errors.log", "a") as log: log.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {error_info}\n") import traceback traceback.print_exc(file=log) self.master.after(0, lambda: messagebox.showerror( "搜索错误", f"发生严重错误: {error_info}\n详细信息已记录到日志" )) finally: self.master.after(0, lambda: self.search_button.config(state=tk.NORMAL)) self.master.after(0, lambda: self.stop_button.config(state=tk.DISABLED)) self.search_thread = None def search_in_text_file(self, filepath, pattern): """在文本文件中搜索匹配项""" matches = [] try: encoding = self.detect_encoding(filepath) try: with open(filepath, 'r', encoding=encoding, errors='replace') as f: for line_num, line in enumerate(f, 1): if pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except UnicodeDecodeError: # 特殊编码处理回退 with open(filepath, 'rb') as f: content = f.read() try: text = content.decode('utf-8', errors='replace') except: text = content.decode('latin-1', errors='replace') for line_num, line in enumerate(text.splitlines(), 1): if pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except Exception as e: print(f"读取文本文件失败 {filepath}: {str(e)}") return matches def search_in_office_file(self, filepath, pattern): """在Office文件中搜索文本内容""" matches = [] _, ext = os.path.splitext(filepath) ext_lower = ext.lower() try: # DOCX文件处理 if ext_lower == '.docx': doc = docx.Document(filepath) # 搜索段落 for i, para in enumerate(doc.paragraphs, 1): if para.text and pattern.search(para.text): matches.append((i, f"段落 {i}: {para.text[:100]}" + ("..." if len(para.text) > 100 else ""))) # 搜索表格 for table in doc.tables: for row_idx, row in enumerate(table.rows, 1): for cell_idx, cell in enumerate(row.cells, 1): if cell.text and pattern.search(cell.text): content = cell.text.strip() if len(content) > 100: content = content[:100] + "..." matches.append((row_idx, f"表格 行{row_idx}列{cell_idx}: {content}")) # XLSX/XLS文件处理 elif ext_lower in ('.xlsx', '.xls', '.xlsm'): # 处理新格式Excel文件 if ext_lower in ('.xlsx', '.xlsm'): wb = load_workbook(filepath, read_only=True, data_only=True) for sheet_name in wb.sheetnames: sheet = wb[sheet_name] for row_idx, row in enumerate(sheet.iter_rows(values_only=True), 1): for col_idx, cell in enumerate(row, 1): if cell is not None and pattern.search(str(cell)): cell_ref = f"{chr(64+col_idx)}{row_idx}" cell_value = str(cell).strip() if len(cell_value) > 100: cell_value = cell_value[:100] + "..." matches.append((row_idx, f"工作表 '{sheet_name}' 单元格 {cell_ref}: {cell_value}")) # 处理旧格式Excel文件 elif ext_lower == '.xls': wb = xlrd.open_workbook(filepath) for sheet_idx in range(wb.nsheets): sheet = wb.sheet_by_index(sheet_idx) for row_idx in range(sheet.nrows): for col_idx in range(sheet.ncols): cell = sheet.cell_value(row_idx, col_idx) if cell and pattern.search(str(cell)): cell_ref = f"{chr(65+col_idx)}{row_idx+1}" cell_value = str(cell).strip() if len(cell_value) > 100: cell_value = cell_value[:100] + "..." matches.append((row_idx+1, f"工作表 '{sheet.name}' 单元格 {cell_ref}: {cell_value}")) # PPTX文件处理 elif ext_lower == '.pptx': from pptx import Presentation ppt = Presentation(filepath) # 搜索幻灯片文本 for slide_idx, slide in enumerate(ppt.slides, 1): for shape in slide.shapes: if hasattr(shape, "text"): if shape.text and pattern.search(shape.text): content = shape.text.strip() if len(content) > 100: content = content[:100] + "..." matches.append((slide_idx, f"幻灯片 {slide_idx}: {content}")) # PDF文件处理 elif ext_lower == '.pdf': with open(filepath, 'rb') as f: pdf = PyPDF2.PdfReader(f) for page_num in range(len(pdf.pages)): page_text = pdf.pages[page_num].extract_text() if page_text and pattern.search(page_text): # 提取匹配内容 matches_found = [] for match in pattern.finditer(page_text): context = page_text[max(0, match.start()-20):match.end()+20] context = context.replace('\n', ' ').strip() matches_found.append(context) # 添加到结果 if matches_found: preview = "; ".join(matches_found[:3]) # 显示前3个匹配 if len(matches_found) > 3: preview += f" ... (+{len(matches_found)-3} 更多)" matches.append((page_num+1, f"页面 {page_num+1}: {preview}")) # 旧版DOC文件处理 elif ext_lower == '.doc': try: # 尝试使用antiword转换DOC为文本 result = subprocess.run(['antiword', filepath], capture_output=True, text=True, timeout=10) if result.returncode == 0: doc_text = result.stdout for line_num, line in enumerate(doc_text.split('\n'), 1): if line and pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except Exception: # 备用方法:使用python-doc处理 import win32com.client word = win32com.client.Dispatch("Word.Application") word.Visible = False doc = word.Documents.Open(filepath) doc_text = doc.Content.Text doc.Close() word.Quit() for line_num, line in enumerate(doc_text.split('\n'), 1): if line and pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except Exception as e: print(f"处理Office文件失败 {filepath}: {str(e)}") return matches def search_in_archive(self, filepath, pattern): """在压缩文件中搜索匹配项""" matches = [] _, ext = os.path.splitext(filepath) ext_lower = ext.lower() try: # ZIP文件处理 if ext_lower in ('.zip', '.jar', '.war'): with zipfile.ZipFile(filepath, 'r') as archive: for name in archive.namelist(): # 只处理文本文件和Office文档 if not name.endswith(('/')) and not self.is_binary(name): try: with archive.open(name) as file: content = file.read(4096) # 只读取前4KB # 尝试检测编码 result = chardet.detect(content) encoding = result['encoding'] if result['confidence'] > 0.7 else 'utf-8' # 解码内容并搜索 try: text_content = content.decode(encoding, errors='replace') if pattern.search(text_content): matches.append((name, f"压缩文件中的文件: {name}")) except: # 二进制内容搜索 if pattern.search(content): matches.append((name, f"压缩文件中的文件(二进制内容): {name}")) except Exception: continue # 其他压缩格式(需要外部工具) elif ext_lower in ('.rar', '.7z', '.tar', '.gz'): # 使用7zip命令行工具解压并搜索 temp_dir = tempfile.mkdtemp() try: subprocess.run(['7z', 'x', filepath, f'-o{temp_dir}'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, timeout=60) # 递归搜索解压的目录 for root, _, files in os.walk(temp_dir): for file in files: full_path = os.path.join(root, file) _, file_ext = os.path.splitext(file) file_ext = file_ext.lower() # 只在文本/Office文件中搜索 if file_ext in ['', '.txt', '.py', '.java', '.c', '.cpp', '.h', '.html', '.xml', '.json', '.csv', '.docx', '.xlsx', '.pptx', '.pdf']: if file_ext in ['.docx', '.xlsx', '.pptx', '.pdf']: file_matches = self.search_in_office_file(full_path, pattern) else: file_matches = self.search_in_text_file(full_path, pattern) if file_matches: matches.append((file, f"压缩文件中的文件: {file}")) finally: shutil.rmtree(temp_dir, ignore_errors=True) except Exception as e: print(f"处理压缩文件失败 {filepath}: {str(e)}") return matches def detect_encoding(self, filepath): """改进的文件编码检测方法""" try: # 尝试读取文件前4KB进行编码检测 with open(filepath, 'rb') as f: raw_data = f.read(4096) # 使用chardet进行编码检测 result = chardet.detect(raw_data) # 优先使用检测到的编码,否则尝试常见编码 if result['confidence'] > 0.7: return result['encoding'] # 中文环境常用编码回退策略 common_encodings = ['utf-8', 'gbk', 'gb2312', 'gb18030', 'latin1'] for encoding in common_encodings: try: # 尝试解码验证 raw_data.decode(encoding, errors='strict') return encoding except UnicodeDecodeError: continue # 默认使用UTF-8 return 'utf-8' except Exception: return 'utf-8' def is_binary(self, filepath): """检查文件是否为二进制文件""" try: with open(filepath, 'rb') as f: chunk = f.read(1024) if b'\0' in chunk: # 空字节是二进制文件的标志 return True # 检查高字节值 if any(byte >= 0x80 for byte in chunk): return True return False except: return False def stop_search(self): """停止当前搜索""" self.stop_requested = True self.status_label.config(text="正在停止搜索...") self.stop_button.config(state=tk.DISABLED) # 冻结进度条显示当前进度 self.progress_bar.config(mode='indeterminate' if self.progress_var.get() == 0 else 'determinate') def export_results(self): """导出搜索结果""" if not self.results: messagebox.showinfo("导出结果", "没有可导出的搜索结果") return file_path = filedialog.asksaveasfilename( title="保存搜索结果为", defaultextension=".csv", filetypes=[("CSV 文件", "*.csv"), ("文本文件", "*.txt")] ) if not file_path: return try: with open(file_path, 'w', encoding='utf-8') as f: # 写出CSV头部 f.write("文件路径,匹配行号,匹配内容\n") # 写出每项结果 for file, matches in self.results.items(): for line_num, match_content in matches: # 清理内容中的逗号 cleaned_content = match_content.replace('"', '""').replace(',', ';') f.write(f'"{file}",{line_num},"{cleaned_content}"\n') messagebox.showinfo("导出成功", f"搜索结果已保存到:\n{file_path}") except Exception as e: messagebox.showerror("导出错误", f"导出失败: {str(e)}") def show_file_content(self, event=None): """在预览区域显示文件内容""" # 获取选中的文件 selection = self.file_tree.selection() if not selection: return selected_item = selection[0] filepath = self.file_tree.item(selected_item, 'values')[1] # 清空预览区域 self.content_text.delete(1.0, tk.END) # 获取文件扩展名 _, ext = os.path.splitext(filepath) ext_lower = ext.lower() # 显示文件路径标题 self.content_text.insert(tk.END, f"文件路径: {filepath}\n", "header") # 处理不同文件类型 try: # 处理Office文档 if ext_lower in ['.docx', '.xlsx', '.xls', '.xlsm', '.pptx', '.pdf', '.doc']: matches = self.results.get(filepath, []) if not matches: self.content_text.insert(tk.END, "\n未找到匹配内容\n", "warning") return self.content_text.insert(tk.END, f"\n找到 {len(matches)} 个匹配项:\n\n", "header") # 显示每个匹配项 for i, (line_num, content) in enumerate(matches, 1): self.content_text.insert(tk.END, f"[匹配项 {i}] 位置: {line_num}\n") self.content_text.insert(tk.END, f"{content}\n\n") # 处理压缩文件 elif ext_lower in ['.zip', '.rar', '.7z', '.tar', '.gz']: matches = self.results.get(filepath, []) if not matches: self.content_text.insert(tk.END, "\n未找到匹配内容\n", "warning") return self.content_text.insert(tk.END, f"\n找到 {len(matches)} 个匹配项:\n\n", "header") for i, (file_in_zip, content) in enumerate(matches, 1): self.content_text.insert(tk.END, f"[匹配项 {i}] 文件: {file_in_zip}\n") self.content_text.insert(tk.END, f"{content}\n\n") # 处理文本文件 else: # 获取关键词高亮模式 keyword = self.keyword_entry.get().strip() flags = re.IGNORECASE if self.case_var.get() else 0 if self.regex_var.get(): try: pattern = re.compile(keyword, flags) except: pattern = None else: pattern = re.compile(re.escape(keyword), flags) # 显示文件内容并高亮匹配 self.content_text.insert(tk.END, "\n文件内容:\n\n", "header") # 限制预览内容大小(最多显示1000行) max_preview_lines = 1000 try: encoding = self.detect_encoding(filepath) with open(filepath, 'r', encoding=encoding, errors='replace') as f: line_count = 0 for line in f: line_count += 1 if line_count > max_preview_lines: self.content_text.insert(tk.END, f"\n... (文件过大,仅显示前{max_preview_lines}行)\n", "warning") break # 插入行号 self.content_text.insert(tk.END, f"{line_count:4d} | ", "linenum") # 插入行内容并高亮匹配 if pattern: start_idx = 0 for match in pattern.finditer(line): # 插入匹配前的文本 self.content_text.insert(tk.END, line[start_idx:match.start()]) # 插入高亮的匹配文本 self.content_text.insert(tk.END, match.group(), "match") start_idx = match.end() # 插入匹配后的文本 self.content_text.insert(tk.END, line[start_idx:]) else: self.content_text.insert(tk.END, line) except UnicodeDecodeError: self.content_text.insert(tk.END, "\n无法解码此文件内容(可能是二进制文件)\n", "warning") except Exception as e: self.content_text.insert(tk.END, f"\n读取文件时出错: {str(e)}\n", "warning") except Exception as e: self.content_text.insert(tk.END, f"\n加载文件内容出错: {str(e)}\n", "warning") def open_selected_file(self, event=None): """用系统默认程序打开选中的文件""" selection = self.file_tree.selection() if not selection: return selected_item = selection[0] filepath = self.file_tree.item(selected_item, 'values')[1] try: if sys.platform == "win32": os.startfile(filepath) elif sys.platform == "darwin": # macOS subprocess.run(["open", filepath]) else: # Linux subprocess.run(["xdg-open", filepath]) except Exception as e: messagebox.showerror("打开文件失败", f"无法打开文件: {str(e)}") def open_file_location(self): """在文件资源管理器中打开文件所在位置""" selection = self.file_tree.selection() if not selection: return selected_item = selection[0] filepath = self.file_tree.item(selected_item, 'values')[1] folder = os.path.dirname(filepath) try: if sys.platform == "win32": subprocess.run(["explorer", "/select,", filepath]) elif sys.platform == "darwin": # macOS subprocess.run(["open", "-R", filepath]) else: # Linux subprocess.run(["xdg-open", folder]) except Exception as e: messagebox.showerror("打开位置失败", f"无法打开位置: {str(e)}") def show_file_context_menu(self, event): """显示文件列表的右键菜单""" item = self.file_tree.identify_row(event.y) if item: self.file_tree.selection_set(item) self.file_menu.tk_popup(event.x_root, event.y_root) def copy_selected_text(self): """复制预览区域中选中的文本""" selected_text = self.content_text.get(tk.SEL_FIRST, tk.SEL_LAST) if selected_text: self.master.clipboard_clear() self.master.clipboard_append(selected_text) # 程序入口 if __name__ == "__main__": root = tk.Tk() app = FileSearchApp(root) # 添加图标(如果有) try: if sys.platform == "win32": root.iconbitmap("search_icon.ico") else: img = tk.PhotoImage(file="search_icon.png") root.iconphoto(True, img) except: pass root.mainloop() 检查问题,搜索出错
09-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值