簡體   English   中英

tkinter 文本小部件:如何在軟換行后自動縮進

[英]tkinter Text Widget: how to indent automatically after a soft line wrap

在行開頭輸入連字符時,我希望 Text 小部件將以下文本視為后續行縮進的部分,如下所示:

  • 通過輸入這個連字符(我的意思是連字符,而不是自動生成的那個項目符號)要縮進的部分開始。 當到達右邊界時,將執行軟換行(我使用的是 wrap=tk.WORD)並且新行應該看起來像這里一樣。

相反,新行從左邊框開始,沒有縮進。

在 Text 小部件的屬性中,我發現 spacing1、spacing2、spacing3 都是指垂直行距。 沒有提示水平間距。 我是否缺少適當的屬性,還是必須從頭開始編寫所需的行為?

如果是這樣,我將不得不綁定到換行事件,但我認為沒有。

有誰知道如何解決這個問題?

您可以將屬性lmargin1lmargin2添加到標簽,然后將該標簽應用於文本范圍以控制左邊距。

這是一個示例(如果您使用不是命名字體的自定義字體,則必須調整查詢字體的部分):

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM