You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 06:09:32