Tkinter Text组件bgstipple加载自定义XBM时出现位置偏移异常
问题描述
运行下述Python脚本可生成仅包含单条竖线的XBM位图文件,支持将竖线放置在位图的左侧、居中、右侧位置。但将该自定义XBM文件赋值给Tkinter Text组件的bgstipple属性时,会出现固定异常:
- 图案整体偏移半个字符宽度:XBM内竖线设为左对齐时,stipple效果中竖线显示在字符居中位置;XBM内竖线设为居中对齐时,stipple效果中竖线显示在字符末尾位置
- 竖线设为右对齐时完全无法显示,且图案无预期的按字符折行环绕效果
使用Tkinter内置XBM文件时不会触发该异常,下方代码可100%复现问题:
import tkinter as tk, tkinter.font as tkf import math class Text(tk.Text): def __init__(self, master, **kwargs): tk.Text.__init__(self, master, **dict(font='{Courier New} 14',wrap="none",exportselection=1,takefocus=1)) self.make_caret_xbm('caret.xbm', self['font']) def make_caret_xbm(self, path, font, inswidth=1) -> None: #prepare f = tkf.Font(font=font) h, w = f.metrics('linespace'), f.measure(' ') d, m = divmod(w, 8) bl = d+(m>0) wtmz = f'{"0"*(bl*8)}' #line at beginning ~ appears in center of character lt = f'{"1"*inswidth}{wtmz}' #line in middle ~ appears at end of character cn = f'{"0"*(math.ceil(w/2))}{"1"*inswidth}{wtmz}' #line at end ~ does not appear rt = f'{"0"*(w-inswidth)}{"1"*inswidth}{wtmz}' #---set-line-type-here---vv row = ','.join(hex(int( lt[(n:=int(i*8)):n+8] ,2)) for i in range(bl)) xbmdata = ',\n\t'.join(row for i in range(h)) xbmdata = (f"#define image_width {w}\n#define image_height {h}\n" "static unsigned char image_bits[] = {\n\t" f'{xbmdata}}};') print(xbmdata) #save xbm with open(path, 'wb') as f: f.write(xbmdata.encode()) def caret(self): #create the caret tag and add it to everything self.tag_configure('CARET',bgstipple="@caret.xbm",background='#FF0000') self.tag_add('CARET','1.0','end') if __name__ == '__main__': class App(tk.Tk): def __init__(self, *args, **kwargs): tk.Tk.__init__(self, *args, **kwargs) #config cell self.columnconfigure(0, weight=1) self.rowconfigure (0, weight=1) #instantiate editor (ed := Text(self)).grid(sticky='nswe') #insert text and apply caret to everything ed.insert('1.0', 'Hello World') ed.caret() #run App().mainloop()
异常原因
异常由两个核心问题共同导致:
- XBM位序生成错误:XBM格式规定单字节存储的8个像素,遵循最低有效位(LSB)对应最左侧像素、最高有效位(MSB)对应最右侧像素的规则。原代码直接将从左到右排列的8位像素字符串转为整数,相当于把每个字节内的像素顺序完全反转,直接造成了半个字节(4像素,约半个等宽字符宽度)的固定偏移。
- 多余填充位+stipple平铺规则:Tk的stipple图案是从组件绘制原点开始全局平铺,不会自动按单个字符单元格对齐。原代码额外拼接了全0填充串,右对齐的竖线刚好落入XBM声明宽度外的填充区域,渲染时直接被裁剪,因此完全不显示。
修正方法
只需调整XBM生成逻辑,匹配XBM格式规范即可:
- 每8位像素切片转换为整数前,先将切片字符串反转,符合LSB优先的存储规则
- 移除多余的尾部全0填充逻辑,确保所有有效像素都落在XBM声明的宽度范围内
修正后的make_caret_xbm方法核心代码如下:
def make_caret_xbm(self, path, font, inswidth=1) -> None: f = tkf.Font(font=font) h, w = f.metrics('linespace'), f.measure(' ') # 计算每行需要的字节数,不足8位的部分补0凑整字节 byte_per_row = (w + 7) // 8 total_bit_per_row = byte_per_row * 8 # 左对齐竖线 lt = '1'*inswidth + '0'*(total_bit_per_row - inswidth) # 居中竖线 offset = (w - inswidth)//2 cn = '0'*offset + '1'*inswidth + '0'*(total_bit_per_row - offset - inswidth) # 右对齐竖线 offset = w - inswidth rt = '0'*offset + '1'*inswidth + '0'*(total_bit_per_row - offset - inswidth) # 选择要生成的竖线位置,注意每8位反转后转整数,匹配XBM的LSB规则 target_bits = lt row_bytes = [] for i in range(byte_per_row): bit_seg = target_bits[i*8 : (i+1)*8] # 反转位顺序后转整数 row_bytes.append(hex(int(bit_seg[::-1], 2))) row = ','.join(row_bytes) xbmdata = ',\n\t'.join(row for _ in range(h)) xbmdata = (f"#define image_width {w}\n#define image_height {h}\n" "static unsigned char image_bits[] = {\n\t" f'{xbmdata}}};') with open(path, 'wb') as f: f.write(xbmdata.encode())
替换原有方法后,三个位置的竖线都可以正常显示,stipple图案也会按预期平铺。
内容的提问来源于stack exchange,提问作者OysterShucker
相关产品推荐
相关产品推荐

