[英]tkinter Text Widget: how to indent automatically after a soft line wrap
在行開頭輸入連字符時,我希望 Text 小部件將以下文本視為后續行縮進的部分,如下所示:
相反,新行從左邊框開始,沒有縮進。
在 Text 小部件的屬性中,我發現 spacing1、spacing2、spacing3 都是指垂直行距。 沒有提示水平間距。 我是否缺少適當的屬性,還是必須從頭開始編寫所需的行為?
如果是這樣,我將不得不綁定到換行事件,但我認為沒有。
有誰知道如何解決這個問題?
您可以將屬性lmargin1
和lmargin2
添加到標簽,然后將該標簽應用於文本范圍以控制左邊距。
這是一個示例(如果您使用不是命名字體的自定義字體,則必須調整查詢字體的部分):
import tkinter as tk
from tkinter import font
root = tk.Tk()
text = tk.Text(root, wrap="word", width=40, height=10)
text.pack(fill="both", expand=True)
text_font = font.nametofont(text.cget("font"))
bullet_width = text_font.measure("- ")
em = text_font.measure("m")
text.tag_configure("bulleted", lmargin1=em, lmargin2=em+bullet_width)
message = (
"by entering this hyphen (I really mean hyphen, not that bullet "
"that is automatically generated) the section to be indented starts. "
"When reaching the right border, a soft wrap is performed (I'm using "
"wrap=tk.WORD) and the new line should look like it does here."
)
text.insert("end", "- " + message, "bulleted")
root.mainloop()
我的版本旨在在您鍵入時工作,允許您傳遞要格式化的文本,以及打開和保存項目符號格式的文件。 代碼的肉和土豆被注釋掉了。
主文件
from configure import *
#used for non-bulleted lines with indention
Sub = chr(8204) #completely invisible but exists
class BulletMenu(ttk.Frame):
@property
def info(self) -> str:
return self.info_lbl['text']
@info.setter
def info(self, text:str):
self.info_lbl['text'] = text
def timestamp(self, ts:bool=False):
self.time = self.time if not ts else ts
if self.time:
self.info = strftime("%I:%M %a %b %d %Y")
self.after(10000, self.timestamp)
def __init__(self, master, row:int=0, column:int=0, **kwargs):
ttk.Frame.__init__(self, master, style='custom.TFrame', **kwargs)
self.grid(row=row, column=column, sticky='nswe', ipady=4)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
#info label
self.info_lbl = ttk.Label(self, style='custom.TLabel', anchor='center')
self.info_lbl.grid(row=0, column=0, sticky='ew')
self.time = True
self.timestamp()
#feature separator
ttk.Separator(self, orient='vertical', style='custom.TSeparator').grid(row=0, column=1, sticky='ns', padx=(4,4))
#filename entry label
ttk.Label(self, text='save as:', style='custom.TLabel').grid(row=0, column=2, sticky='e')
#filename entry
fn_ent = tk.Entry(self, width=32, font='Helvetica 10 bold', **asdict(Entry_dc()))
fn_ent.grid(row=0, column=3, sticky='e', padx=(4,6))
#save button
ttk.Button(self, text="save", style='custom.TButton', command=lambda: master.save(fn_ent.get())).grid(row=0, column=4, sticky='e', padx=(2,2))
#feature separator
ttk.Separator(self, orient='vertical', style='custom.TSeparator').grid(row=0, column=5, sticky='ns', padx=(2,2))
#open button
ttk.Button(self, text="open", style='custom.TButton', command=master.open).grid(row=0, column=6, sticky='e', padx=(2,4))
def displayInfoFor(self, info:str, millis:int=5000):
self.time = False
self.info = info
self.after(millis, lambda: self.timestamp(True))
class BulletEditor(ttk.Frame):
@property
def bytes(self) -> bytes:
return self.tf.get('1.0', 'end-1c').encode('utf-8')
@property
def text(self) -> str:
return self.tf.get('1.0', 'end-1c')
@text.setter
def text(self, text:str):
#delete all text
self.tf.delete('1.0', 'end')
#create a list of lines
lines = text.split('\n')
for ln, line in enumerate(lines):
#insert the new line
self.tf.insert(f'{ln+1}.0', f'{line}\n')
#apply formatting accordingly
if line[0:1] in self.bullet_chr: ##bullet
self.tf.tag_remove("child", f'{ln+1}.0-1c', 'end-1c')
self.tf.tag_add('bullet', f'{ln+1}.0', f'{ln+1}.1')
self.tf.tag_add('parent', f'{ln+1}.1', 'end-1c')
elif line[0:1] == Sub: ##sub
self.tf.tag_add('child', f'{ln+1}.0-1c', 'end')
else: ##normal
self.tf.tag_remove("child", f'{ln+1}.0-1c', 'end')
self.tf.tag_remove("parent", f'{ln+1}.0-1c', 'end')
self.tf.tag_remove("bullet", f'{ln+1}.0-1c', 'end')
def __init__(self, master, row:int=0, column:int=0, **kwargs):
ttk.Frame.__init__(self, master, style='custom.TFrame', **kwargs)
self.grid(row=row, column=column, sticky='nswe')
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
#textfield
self.tf = tk.Text(self, **asdict(Text_dc()))
self.tf.grid(row=0, column=0, sticky='nswe')
#vertical scrollbar
self.vs = ttk.Scrollbar(self, style='arrowless.Vertical.TScrollbar', command=self.tf.yview)
self.vs.grid(row=0, column=1, sticky='nswe')
#attach scrollbar to textfield
self.tf.configure(yscrollcommand=self.vs.set)
#font metric
fontname = self.tf.cget("font").split()[0]
fontsize = self.tf.cget("font").split()[1]
child = font.Font(self, fontname).measure("- ")
#tags
self.tf.tag_configure("parent", lmargin2=child, foreground=Theme.Gloss)
self.tf.tag_configure("bullet", foreground=Theme.Trim, font=f'{fontname} {fontsize} bold')
self.tf.tag_configure("child", lmargin1=child, lmargin2=child, foreground=Theme.Gloss)
#bullets
self.bullet_chr = ['-', '.', '*', '+']
self.bullet_sym = ['minus', 'period', 'asterisk', 'plus']
#events
for b in self.bullet_sym:
self.tf.bind(f'<KeyPress-{b}>', self.applyformat)
self.tf.bind('<KeyPress-Return>', self.applyformat)
self.tf.bind('<Control-Return>', self.applyformat)
def applyformat(self, event:tk.Event):
#current position - 1 (this is before insertion)
position = self.tf.index('insert')
#convert line and character to int
l = int(position.split('.')[0])
c = int(position.split('.')[1])
#apply formatting accordingly
if event.keysym in self.bullet_sym and c == 0: ##bullet
self.tf.tag_remove("child", f'{l}.0-1c', 'end-1c')
self.tf.insert(position, f'{event.char} ')
self.tf.tag_add('bullet', f'{l}.0', f'{l}.1')
self.tf.tag_add('parent', f'{l}.1', 'end-1c')
return 'break'
elif event.keysym == 'Return': ##sub
if event.state & 0x4:
self.tf.insert(position, f'\n{Sub}')
self.tf.tag_add('child', f'{l+1}.0-1c', 'end')
else: ##normal
self.tf.insert(position, f'\n')
self.tf.tag_remove('parent', f'{l+1}.0-1c', 'end')
self.tf.tag_remove("child", f'{l+1}.0-1c', 'end')
self.tf.tag_remove("bullet", f'{l+1}.0-1c', 'end')
self.tf.see(self.tf.index('insert+1l'))
return 'break'
#chain of effects to apply to window close
EffectChain = (Effect_e.Alpha, Effect_e.Width, Effect_e.Height, Effect_e.X, Effect_e.Y)
class App(tk.Tk):
DIR:str = path.join(getcwd(), 'bullets/')
def __init__(self):
tk.Tk.__init__(self)
#init custom theme
CustomTheme()
#simple window effects
Window.ApplyEffects(self, 10, Effect_e.Alpha, ([1],), 20)
Window.EnableCloseEffect(self, 10, EffectChain)
#make editor dominant in size
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
#create directory to store bullets, if it doesn't exist
if not path.isdir(App.DIR):
mkdir(App.DIR)
#instantiate menu
self.menu = BulletMenu(self)
#feature separator
ttk.Separator(self, orient='horizontal', style='stack.TSeparator').grid(row=1, column=0, sticky='nswe', ipady=1)
#instantiate editor
self.editor = BulletEditor(self, 2, 0)
#example of assigning text directly
self.editor.text = (
"* Bullets can be created with '*', '.', '-', and '+'. "
"Bullets will soft wrap to the next line when enough text is supplied."
f"\n{Sub}\n{Sub}Control+Return creates a bulletless, indented and soft-wrapped line. "
"You can use this when you want to add paragraphs under your bullet.\n\n"
"Pressing just Return, breaks out of indentation and hard wraps text. "
"The font color changes, as well.\n\n"
"* You can open and save files. Opening a .bullet file will display "
"formatting identical to what you saw in the editor when you saved. "
".bullet files are automatically saved to AppDirectory/bullet/."
"When you invoke opening of a .bullet file you are automatically brought to AppDirectory/bullet/.\n\n"
"* You do not have to include the file extension in the \"save as:\" Entry field. "
"It is appended automatically when you click save.\n\n"
"Enjoy."
)
def save(self, filename:str):
if filename:
filename = filename.replace('.bullet', '')
with open(f'{App.DIR}{filename}.bullet', 'wb') as f:
f.write(self.editor.bytes)
self.menu.displayInfoFor(f'saved: {filename}.bullet', 2000)
else:
self.menu.displayInfoFor(f'supply a filename', 2000)
def open(self):
bullet:str = filedialog.askopenfilename(initialdir=App.DIR, title="Select Bullet", filetypes=(("bullet files","*.bullet"),("all files", "*.*")))
if bullet:
with open(bullet, 'r') as f:
self.editor.text = re.compile('‌').sub(Sub, f.read())
if "__main__" == __name__:
app = App()
app.configure(**asdict(WindowStyle_dc()))
app.title("Bullet Editor v0.28")
app.geometry('800x600+100+100')
app.attributes('-alpha', 0.0)
app.minsize(640, 480)
app.mainloop()
配置文件
import re
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import font
from tkinter import filedialog
from time import strftime
from os import mkdir, path, getcwd
from dataclasses import dataclass, asdict
from collections import namedtuple
from typing import Tuple, Callable, Union, List, Iterable
from enum import Enum
#declare theme colors
Theme_t = namedtuple('Theme_t', 'Base Primer Topcoat SubContrast Contrast Trim Hilight Accent Flat Gloss')
STACKTHEME = Theme_t('#2d2d2d', '#697075', '#3d3d3d', '#646464', '#f48024', '#71dbfe', '#dad9c6', '#AAAAAA', '#dad9c6', '#e7929a')
#YOURTHEME = Theme_t('#', '#', '#', '#', '#', '#', '#', '#', '#', '#')
Theme = STACKTHEME
''' THEME FONTS '''
FONT = False
FONT1 = 'Fixedsys'
FONT2 = 'Calibri'
FONT3 = 'Helvetica'
FONT4 = 'Consolas'
@dataclass
class Entry_dc:
foreground: str = Theme.Flat
background: str = Theme.Topcoat
borderwidth: str = 2
highlightbackground: str = Theme.Primer
highlightcolor: str = Theme.Primer
insertbackground: str = Theme.Primer
selectforeground: str = Theme.Base
selectbackground: str = Theme.Flat
relief: str = 'flat'
highlightthickness: int = 2
selectborderwidth: int = 0
insertborderwidth: int = 2
insertwidth: int = 2
@dataclass
class Label_dc:
foreground: str = Theme.Contrast
background: str = Theme.Base
font: Tuple = (FONT3, 10, 'bold')
@dataclass
class Button_dc:
foreground: str = Theme.Contrast
background: str = Theme.Base
bordercolor: str = Theme.Base
darkcolor: str = Theme.Base
lightcolor: str = Theme.Base
highlightcolor: str = Theme.Base
relief: str = 'flat'
compound: str = 'left'
highlightthickness: int = 0
shiftrelief: int = 0
width: int = 0
padding = [2,2,2,2] #l,t,r,b
font: Tuple = (FONT3, 10)
anchor: str = 'nw'
@dataclass
class Scrollbar_dc:
background: str = Theme.SubContrast
bordercolor: str = Theme.Topcoat
darkcolor: str = Theme.SubContrast
lightcolor: str = Theme.SubContrast
troughcolor: str = Theme.Topcoat
arrowcolor: str = Theme.Trim
gripcount: int = 0
arrowsize: int = 14
@dataclass
class Separator_dc:
background: str = Theme.Primer
@dataclass
class Text_dc:
background: str = Theme.Topcoat # main element
foreground: str = Theme.Flat
borderwidth: int = 0
selectbackground: str = Theme.Flat # selected text
selectforeground: str = Theme.Base
selectborderwidth: int = 0 # doesn't seem to do anything ~ supposed to be a border on selected text
insertbackground: str = Theme.Hilight # caret
insertborderwidth: int = 0 # border
insertofftime: int = 300 # blink off millis
insertontime: int = 600 # blink on millis
insertwidth: int = 2 # width
highlightbackground:int = Theme.Topcoat # inner border that is activated when the widget gets focus
highlightcolor: int = Theme.Topcoat # color
highlightthickness: int = 0 # thickness
cursor: str = 'xterm'
exportselection: int = 1
font: str = f'{FONT4} 14'
width: int = 16 # characters ~ often this is ignored as Text gets stretched to fill its parent
height: int = 8 # lines ~ " " " " "
padx: int = 8
pady: int = 8
relief: str = 'flat'
wrap: str = 'word' # "none", 'word'
spacing1: int = 0 # space above every line
spacing2: int = 0 # space between every wrapped line
spacing3: int = 0 # space after return from wrap (paragraph)
state: str = 'normal' # NORMAL=Read/Write | DISABLED=ReadOnly
tabs: str = '2.5c'
takefocus: int = 1
undo: bool = True
xscrollcommand: Callable = None
yscrollcommand: Callable = None
@dataclass
class Frame_dc:
background: str = Theme.Base
bordercolor: str = Theme.Base
darkcolor: str = Theme.Base
lightcolor: str = Theme.Base
relief: str = 'raised'
class CustomTheme(ttk.Style):
def __init__(self, basetheme='clam'):
ttk.Style.__init__(self)
#create theme
self.theme_create('custom', basetheme, {
'custom.TSeparator': {
'layout': [],
'configure': asdict(Separator_dc()),
},
'stack.TSeparator': {
'layout': [],
'configure': asdict(Separator_dc(background=Theme.Contrast)),
},
'arrowless.Vertical.TScrollbar': {
'layout': [('Vertical.Scrollbar.trough',
{'sticky': 'ns', 'children': [
('Vertical.Scrollbar.thumb', {'expand': '1', 'sticky': 'nswe'})
]}
)],
'configure': asdict(Scrollbar_dc()), # arrowsize will still size the scrollbar thickness
},
'custom.TButton': {
'configure': asdict(Button_dc()),
'map': {
'background': [('active', Theme.Contrast),('pressed', Theme.Contrast)],
'foreground': [('active', Theme.Base), ('pressed', Theme.Base)],
}
},
'custom.TLabel': {
'configure': asdict(Label_dc()),
},
'custom.TFrame': {
'configure': asdict(Frame_dc()),
},
'custom.TEntry': {
'configure': asdict(Entry_dc()),
},
})
self.theme_use('custom')
#property to add effect to
class Effect_e(Enum):
Width = 0
Height = 1
X = 2
Y = 3
Alpha = 4
#effect type-hint alias
Effects = Union[Effect_e, Iterable[Effect_e]]
@dataclass
class WindowStyle_dc:
bd: int = 0
relief: str = 'flat'
bg: str = 'black'
highlightbackground: str = 'black'
highlightcolor: str = 'black'
highlightthickness: int = 0
padx: int = 0
pady: int = 0
@dataclass
class WindowAttr_dc:
alpha: float = 0
disabled: bool = False
fullscreen: bool = False
toolwindow: bool = False #windows
topmost: bool = True #windows
transparentcolor: str = '#000001'
#modified: bool = False #mac
#titlepath: str = '' #mac
#used to parse root.geometry()
Geometry_re = re.compile('^(\d+)x(\d+)\+(\d+)\+(\d+)$')
#window effects manager
class Window:
@staticmethod
def GetGeometryList(window):
return list(map(int, Geometry_re.search(window.geometry()).groups()))
@staticmethod
def ListSetGeometry(window, geom:List[int]):
sep = ['', 'x', '+', '+']
geo = ''
for g, s in zip(geom, sep):
geo = f'{geo}{s}{g}'
window.geometry(geo)
@staticmethod
def EnableCloseEffect(window, steps:int, effects:Effects, ranges:Iterable[List]=None, millis:int=20):
def close(window, steps, effects, ranges, millis):
geom = Window.GetGeometryList(window)
effects = effects if isinstance(effects, Iterable) else [effects]
if not ranges:
ranges =[]
for e in effects:
if e is Effect_e.Alpha:
ranges.append((1,0))
elif (e.value) < 4:
ranges.append((geom[e.value], 0))
window.minsize(0,0)
Window.ApplyEffects(window, steps, effects, ranges, millis, True)
window.protocol("WM_DELETE_WINDOW", lambda: close(window, steps, effects, ranges, millis))
@staticmethod
def ApplyEffects(window, steps:int, effects:Effects, ranges:Iterable[List], millis:int=20, destroy:bool=False, minsize:Iterable=None, step=0, geom:List[int]=None):
step += 1
effects = effects if isinstance(effects, Iterable) else [effects]
for e, r in zip(effects, ranges):
r.insert(0, 0) if len(r) == 1 else None
amt = (r[1]-r[0])/steps
if e is Effect_e.Alpha:
alpha = (float(window.attributes('-alpha')) + amt) if steps > step else r[1]
window.attributes('-alpha', alpha)
elif (e.value) < 4:
geom = Window.GetGeometryList(window) if not geom else geom
geom[e.value] = int(geom[e.value] + amt) if steps > step else r[1]
if geom:
Window.ListSetGeometry(window, geom)
if step < steps:
window.after(millis, lambda: Window.ApplyEffects(window, steps, effects, ranges, millis, destroy, minsize, step, geom))
else:
if minsize:
window.minsize(*minsize)
if destroy:
window.destroy()
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.