Python PySimpleGUI实现点击按钮选中Multiline内指定文本
PySimpleGUI多行文本逐段高亮选中实现
需求背景
开发的编织辅助GUI工具可读取黑白图像生成双字符ASCII编织模板,自动统计每行连续相同字符的分段长度,替代手工记录操作。需要在输出窗口实现:点击按钮可逐段选中/高亮Multiline组件内的连续字符块,顺序为每行从左到右,依次选中连续.块、连续#块循环,系统原生选中文本效果或自定义颜色高亮均可。
原有项目完整代码如下:
import PIL.Image import os from itertools import groupby import PySimpleGUI as sg import time def main_window(): #Set theme and layout sg.theme('reddit') layout = [ [sg.Text("This program is designed to turn a black and white image into a double-knit template")], [sg.Text("If your image doesn't display as desired, adjusting the contrast might help.")], [sg.Text("Enter file name + extention (eg. 'image.jpg')"), sg.InputText(size=(50),key='pic')], [sg.Text("Enter the size you'd like in # of stitches"), sg.Text("Width: "), sg.InputText(size=(10),key='wth'), sg.Text("Length: "), sg.InputText(size=(10),key='lth')], [sg.Text("Contrast"), sg.Slider(range=(1,11), default_value=6, orientation='h', key='sli')], [sg.Button('Generate')], [sg.ProgressBar(50, orientation='h', border_width=4, key='pbar')] ] window = sg.Window('Knitify', layout) #Contrast variables chars1 = ["#", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."] chars2 = ["#", "#", ".", ".", ".", ".", ".", ".", ".", ".", "."] chars3 = ["#", "#", "#", ".", ".", ".", ".", ".", ".", ".", "."] chars4 = ["#", "#", "#", "#", ".", ".", ".", ".", ".", ".", "."] chars5 = ["#", "#", "#", "#", "#", ".", ".", ".", ".", ".", "."] chars6 = ["#", "#", "#", "#", "#", "#", ".", ".", ".", ".", "."] chars7 = ["#", "#", "#", "#", "#", "#", "#", ".", ".", ".", "."] chars8 = ["#", "#", "#", "#", "#", "#", "#", "#", ".", ".", "."] chars9 = ["#", "#", "#", "#", "#", "#", "#", "#", "#", ".", "."] chars10 = ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "."] chars11 = ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#"] while True: event, values = window.read() print(event, values) #Get contrast slider value i = int(values['sli']) #Get Image file p = str(values['pic']) #Grab contrast level if i == 1: con = chars1 elif i ==2: con = chars2 elif i ==3: con = chars3 elif i ==4: con = chars4 elif i ==5: con = chars5 elif i ==6: con = chars6 elif i ==7: con = chars7 elif i ==8: con = chars8 elif i ==9: con = chars9 elif i ==10: con = chars10 elif i ==11: con = chars11 if event == sg.WIN_CLOSED: break if event == 'Generate': #Check if given image file exists, if not, do nothing, wait for valid image. if os.path.exists(p): #Check if output file already exists, if it does, delete and replace with newly generated image. if os.path.exists('convertedImage.txt'): os.remove('convertedImage.txt') #Open and prepare image with Pillow path = values['pic'] img = PIL.Image.open(path) width, height = img.size aspect_ratio = height/width #Get user to enter desired width and height (in stitches) of output image new_width = int(values['wth']) new_height = int(values['lth']) #If values are impossible to deal with then terminate and wait for correct values if new_width <= 0: break if new_height <= 0: break #Resize and convert image to ASCII img = img.resize((int(new_width), int(new_height))) img = img.convert('L') pixels = img.getdata() new_pixels = [con[pixel//25] for pixel in pixels] new_pixels = ''.join(new_pixels) new_pixels_count = len(new_pixels) ascii_image = [new_pixels[index:index + new_width] for index in range(0, new_pixels_count, new_width)] ascii_image = "\n".join(ascii_image) #Save ASCII image to temp file with open('testout.txt', 'w') as f: f.write(ascii_image) f.close() #Use temp file to scan each line and number alternating stitches in each row with open('testout.txt', 'r') as f: for line in map(lambda l: l.strip(), f): runs = [sum(1 for _ in g) for _, g in groupby(line)] x = f"{line} {' '.join(map(str, runs))}" #Write result to new file with open("convertedImage.txt", 'a') as n: n.write("\n" + x) n.close() f.close() #Remove temp file os.remove('testout.txt') #Progress bar bit bar = 1 for i in range(8): window['pbar'].update_bar(bar) bar = bar+bar time.sleep(0.2) #Open second window output_window() window['pbar'].update_bar(0) window.close() def output_window(): #Read contents of converted image with open('convertedImage.txt', 'r') as f: contents = f.read() f.close() #Find the length of the longest line and set multiline box to that width. #Paste the contents of convertedImage to multiline box long = max(open('convertedImage.txt'), key=len) layout = [ [sg.Multiline(size=(len(long),50), font='Consolas', default_text=contents)] ] window = sg.Window("Knitified", layout, modal=True) while True: event, values = window.read() if event == sg.WIN_CLOSED: break main_window()
实现方案
PySimpleGUI的Multiline组件底层封装的是Tkinter原生Text控件,不需要依赖框架暴露的高层接口,直接操作底层控件即可实现选中/高亮效果,具体修改逻辑:
- 给Multiline组件设置唯一key,窗口初始化时加上
finalize=True参数,允许窗口渲染完成前操作控件 - 提前预处理所有行的连续字符段信息,用
groupby计算每一段的字符类型、起始索引、结束索引,存为列表方便快速定位 - 添加段切换按钮,点击时通过底层Text控件的标签方法实现自定义高亮,或直接调用选中方法实现系统原生选中文本效果,同时自动滚动到选中位置
- 维护当前行、当前段的位置状态,点击按钮时按顺序切换段,遍历完当前行自动切到下一行开头,遍历完所有内容自动回到开头
直接替换原代码中output_window函数为以下版本即可:
def output_window(): # 读取文件内容,预处理行和段信息 with open('convertedImage.txt', 'r') as f: lines = [line.rstrip('\n') for line in f if line.strip()] line_segments = [] for line in lines: segs = [] start_idx = 0 for char, group in groupby(line): seg_len = sum(1 for _ in group) segs.append((char, start_idx, start_idx + seg_len)) start_idx += seg_len line_segments.append(segs) # 计算最长行长度设置组件宽度 max_line_len = max(len(line) for line in lines) layout = [ [sg.Multiline(size=(max_line_len, 50), font='Consolas', default_text='\n'.join(lines), key='-MLINE-')], [sg.Button('上一段', key='-PREV_SEG-'), sg.Button('下一段', key='-NEXT_SEG-')] ] # 加finalize=True才能操作底层控件 window = sg.Window("Knitified", layout, modal=True, finalize=True) mline_widget = window['-MLINE-'].Widget # 配置自定义高亮样式,颜色可自行修改 mline_widget.tag_config('seg_highlight', background='#2563eb', foreground='white') # 初始化当前定位状态 current_line = 0 current_seg = 0 def render_current_segment(): # 清除之前的高亮和选中 mline_widget.tag_remove('seg_highlight', '1.0', sg.tk.END) mline_widget.tag_remove('sel', '1.0', sg.tk.END) if current_line >= len(line_segments): return _, start, end = line_segments[current_line][current_seg] # Tkinter文本索引格式为"行号.列号",行号从1开始计数 pos_start = f"{current_line + 1}.{start}" pos_end = f"{current_line + 1}.{end}" # 自定义高亮效果,要换成系统原生选中就注释下一行,放开下面tag_add('sel')的行 mline_widget.tag_add('seg_highlight', pos_start, pos_end) # 系统原生选中文本效果 # mline_widget.tag_add('sel', pos_start, pos_end) # 自动滚动到选中位置 mline_widget.see(pos_start) # 默认加载时显示第一段 render_current_segment() while True: event, values = window.read() if event == sg.WIN_CLOSED: break if event == '-NEXT_SEG-': current_seg += 1 # 当前行段遍历完切下一行 if current_seg >= len(line_segments[current_line]): current_seg = 0 current_line += 1 # 所有内容遍历完回到开头 if current_line >= len(line_segments): current_line = 0 render_current_segment() if event == '-PREV_SEG-': current_seg -= 1 if current_seg < 0: current_line -= 1 if current_line < 0: current_line = len(line_segments) - 1 current_seg = len(line_segments[current_line]) - 1 render_current_segment() # 窗口关闭时清理临时文件 if os.path.exists('convertedImage.txt'): os.remove('convertedImage.txt') window.close()
补充说明
- 两种高亮模式可按需选择:自定义高亮可以自由调整背景、文字颜色,不会和用户手动选中文本的操作冲突;原生选中效果和系统文本选中样式完全一致
- 按钮逻辑支持向前、向后逐段切换,核对编织针数时可以随时回看之前的段
- 自动滚动逻辑保证当前选中的段始终在可视区域内,不用手动拖动滚动条找位置
内容的提问来源于stack exchange,提问作者SatsumaSegment
相关产品推荐
相关产品推荐

