记录实用有价值的内容

tkinter 应用程序:文本编辑器

基于 tkinter 的简易IDE/文本编辑器。(参考

功能:

  1. 菜单栏
  2. 文件操作功能:新建、保存、另存为
  3. 文本操作功能:复制、剪切、全选、撤销、重做
  4. 快捷键
  5. 主题切换
  6. 文本右键功能
  7. 文本高亮
  8. 搜索
  9. 行号

图标:

预览:


配置文件:config.ini


[window]
program_name = UINOTE Editor

[icon]
newfile = ico/newfile.png
openfile = ico/open.png
savefile = ico/save.png
undofile = ico/undo.png
redofile = ico/redo.png
cutfile = ico/cut.png
copyfile = ico/copy.png
pastefile = ico/paste.png
findfile = ico/find.png

[textension]
aa = save
bb = D1D4D1

[theme]
color_schemes = {
    'Default': '#000000.#FFFFFF',
    'Greygarious': '#83406A.#D1D4D1',
    'Aquamarine': '#5B8340.#D1E7E0',
    'Bold Beige': '#4B4620.#FFF0E1',
    'Cobalt Blue': '#ffffBB.#3333aa',
    'Olive Green': '#D1E7E0.#5B8340',
    'Night Mode': '#FFFFFF.#000000'}
select_themes = Default

[common]
bgstartsetting = False
bgdir = bg
bgname = default_bg.png


代码:app.py


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter.filedialog import *
from tkinter import *
from pathlib import Path
from PIL import Image, ImageTk
import tkinter.messagebox as tmb
import shutil
from configparser import ConfigParser
import os.path

d = os.path.dirname(__file__)
file_name = None


def exitText(event=None):
    # 保存  不保存
    if tmb.askokcancel("Quit?", "Really quit?"):
        ## 退出前自动保存文件
        save()
        root.destroy()


def cut():
    '''剪切'''
    content_text.event_generate("<<Cut>>")


def copy():
    '''复制'''
    content_text.event_generate("<<Copy>>")


def paste():
    '''粘贴'''
    content_text.event_generate("<<Paste>>")


def undo(event=None):
    '''向前撤销'''
    content_text.event_generate("<<Undo>>")
    return 'break'


def redo(event=None):
    '''向后返回'''
    content_text.event_generate("<<Redo>>")
    return 'break'


def select_all(event=None):
    '''全选'''
    content_text.tag_add('sel', '1.0', 'end')
    return 'break'


def find_text(event=None):
    '''搜索字段'''
    search_toplevel = Toplevel(root)
    search_toplevel.title('Find Text')
    search_toplevel.transient(root)
    search_toplevel.resizable(False, False)
    Label(search_toplevel, text="Find All:").grid(row=0, column=0, sticky='e')
    search_entry_widget = Entry(search_toplevel, width=25)
    search_entry_widget.grid(row=0, column=1, padx=2, pady=2, sticky='we')
    search_entry_widget.focus_set()
    ignore_case_value = IntVar()
    Checkbutton(search_toplevel, text='IgnoreCase',
                variable=ignore_case_value).grid(row=1,
                                                 column=1,
                                                 sticky='e',
                                                 padx=2,
                                                 pady=2)
    Button(search_toplevel,
           text="Find All",
           underline=0,
           command=lambda: search_output(search_entry_widget.get(
           ), ignore_case_value.get(), content_text, search_toplevel,
                                         search_entry_widget)).grid(
                                             row=0,
                                             column=2,
                                             sticky='ew',
                                             padx=2,
                                             pady=2)

    def close_search_window():
        '''关闭选框'''
        content_text.tag_remove('match', '1.0', 'end')
        search_toplevel.destroy()
        search_toplevel.protocol('WM_DELETE_WINDOW', close_search_window)
        return 'break'


def search_output(needle, if_ignore_case, content_text, search_toplevel,
                  search_box):
    '''匹配文字'''
    content_text.tag_remove('match', '1.0', 'end')
    matches_found = 0
    if needle:
        start_pos = '1.0'
        while True:
            start_pos = content_text.search(needle,
                                            start_pos,
                                            nocase=if_ignore_case,
                                            stopindex='end')
            if not start_pos:
                break
            end_pos = '{}+{}c'.format(start_pos, len(needle))
            content_text.tag_add('match', start_pos, end_pos)
            matches_found += 1
            start_pos = end_pos
            content_text.tag_config('match',
                                    foreground='red',
                                    background='yellow')
            search_box.focus_set()
            search_toplevel.title('{} matches found'.format(matches_found))


def open_file(event=None):
    '''打开文件'''
    input_file_name = askopenfilename(defaultextension=".txt",
                                      filetypes=[("All Files", "*.*"),
                                                 ("Text Documents", "*.txt")])
    if input_file_name:
        global file_name
        file_name = input_file_name
        root.title('{} - {}'.format(os.path.basename(file_name), PROGRAM_NAME))
        content_text.delete(1.0, 'end')
        with open(file_name) as _file:
            content_text.insert(1.0, _file.read())
    update_line_numbers()


def save(event=None):
    '''save文件'''
    print('保存')
    global file_name
    print('文件路径', file_name)
    try:
        if not file_name:
            print('新建,未打开文件')
            save_as()
        else:
            print('保存,已打开文件')
            write_to_file(file_name)
    except:
        pass
        tmb.showerror('异常', '文件保存异常,请重试!')
    return 'break'


def save_as(event=None):
    '''另存为'''
    print('另存为')
    input_file_name = asksaveasfilename(defaultextension=".txt",
                                        filetypes=[("All Files", "*.*"),
                                                   ("Text Documents", "*.txt")
                                                   ])
    if input_file_name:
        global file_name
        file_name = input_file_name
        write_to_file(file_name)
        root.title('{} - {}'.format(os.path.basename(file_name), PROGRAM_NAME))
    return 'break'


def write_to_file(file_name):
    '''写入文件'''
    try:
        content = content_text.get(1.0, 'end')
        with open(file_name, 'w') as the_file:
            the_file.write(content)
    except IOError:
        pass


def new_file(event=None):
    '''新建文件'''
    root.title("Untitled")
    global file_name
    file_name = None
    content_text.delete(1.0, 'end')


def display_about_messagebox(event=None):
    '''about messagebox'''
    tmb.showinfo(
        "About",
        "{}{}".format(PROGRAM_NAME,
                      "\nTkinter GUIApplication\n Development Blueprints"))


def display_help_messagebox(event=None):
    '''help messagebox'''
    tmb.showinfo("Help",
                 "帮助文档: \nTkinter GUI Application\n Development Blueprints",
                 icon='question')


def on_content_changed(event=None):
    '''输入框发送改变时'''
    update_line_numbers()
    update_cursor_info_bar()


def get_line_numbers():
    '''获取行号'''
    output = ''
    if show_line_number.get():
        row, col = content_text.index("end").split('.')
        for i in range(1, int(row)):
            output += str(i) + '\n'
    return output


def update_line_numbers(event=None):
    '''更新行号'''
    line_numbers = get_line_numbers()
    line_number_bar.config(state='normal')
    line_number_bar.delete('1.0', 'end')
    line_number_bar.insert('1.0', line_numbers)
    line_number_bar.config(state='disabled')


def highlight_line(interval=100):
    '''高亮显示当前行'''
    content_text.tag_remove("active_line", 1.0, "end")
    content_text.tag_add("active_line", "insert linestart",
                         "insert lineend+1c")
    content_text.after(interval, toggle_highlight)


def undo_highlight():
    '''取消高亮'''
    content_text.tag_remove("active_line", 1.0, "end")


def toggle_highlight(event=None):
    '''高亮切换'''
    if to_highlight_line.get():
        highlight_line()
    else:
        undo_highlight()


def show_cursor_info_bar():
    '''显示底部行列信息'''
    show_cursor_info_checked = show_cursor_info.get()
    if show_cursor_info_checked:
        cursor_info_bar.pack(expand='no', fill=None, side='right', anchor='se')
    else:
        cursor_info_bar.pack_forget()


def update_cursor_info_bar(event=None):
    '''更新底部行列信息'''
    row, col = content_text.index(INSERT).split('.')
    line_num, col_num = str(int(row)), str(int(col) + 1)  # col starts at 0
    infotext = "Line: {0} | Column: {1}".format(line_num, col_num)
    cursor_info_bar.config(text=infotext)


def change_theme(event=None):
    '''修改主题'''
    selected_theme = themes_choices.get()
    # 写入配置文件
    conf.set('theme', 'select_themes', selected_theme)
    saveConf()
    fg_bg_colors = color_schemes.get(selected_theme)
    foreground_color, background_color = fg_bg_colors.split('.')
    content_text.config(background=background_color, fg=foreground_color)


def select_background(event=None):
    '''选择背景图片,已经取消,因为tkinter透明度不好用'''
    conf.set('common', 'bgstartsetting', 'true')
    saveConf()
    bgStartChecked = bgStart.get()
    print('[*] 是否启用背景设置', bgStartChecked)
    if bgStartChecked:
        print('弹出选择文件框')
        imgSelect = askopenfilename(defaultextension=".txt",
                                    filetypes=[(".png", ".jpg"),
                                               ("Text Documents", "*.txt")])
        if imgSelect:
            global imgSelectName
            imgSelectName = imgSelect
            print(imgSelectName)  # C:/Users/hz/Desktop/我的文件/我的图片/11.jpg
            # 将图片拷贝到当前文件夹   修改配置参数
            shutil.copyfile(imgSelectName, Path(d) / bgDir / bgName)
    else:
        print('隐藏图片')
        print('使用主题选择的颜色')


def set_background():
    '''设置背景'''
    pass


def readSettings():
    '''读取配置文件'''
    global PROGRAM_NAME
    PROGRAM_NAME = conf.get('window', 'PROGRAM_NAME')
    global newFile
    newFile = conf.get('icon', 'newFile')
    global openFile
    openFile = conf.get('icon', 'openFile')
    global saveFile
    saveFile = conf.get('icon', 'saveFile')
    global undoFile
    undoFile = conf.get('icon', 'undoFile')
    global redoFile
    redoFile = conf.get('icon', 'redoFile')
    global cutFile
    cutFile = conf.get('icon', 'cutFile')
    global copyFile
    copyFile = conf.get('icon', 'copyFile')
    global pasteFile
    pasteFile = conf.get('icon', 'pasteFile')
    global findFile
    findFile = conf.get('icon', 'findFile')
    global color_schemes
    color_schemes_str = conf.get('theme', 'color_schemes')
    color_schemes = eval(color_schemes_str)
    global select_themes
    select_themes = conf.get('theme', 'select_themes')
    global bgstartsetting
    bgstartsetting = conf.get('common', 'bgstartsetting')
    global bgDir
    bgDir = conf.get('common', 'bgdir')
    global bgName
    bgName = conf.get('common', 'bgname')


def saveConf():
    '''保存配置文件'''
    fp = Path(d) / 'config.ini'
    with open(fp, 'w') as fw:
        conf.write(fw)


def show_popup_menu(event):
    '''显示右键菜单'''
    popup_menu.tk_popup(event.x_root, event.y_root)


if __name__ == '__main__':
    ## 配置文件路径
    fp = Path(d) / 'config.ini'
    print('config.ini', fp)

    ## 实例化
    conf = ConfigParser()

    ## 读取配置文件
    conf.read(fp, encoding='utf8')  ## 配置文件编码是utf-8

    ## 读取里面的内容
    readSettings()

    ## 开始窗体部分
    root = Tk()

    # 引入菜单
    menu_bar = Menu(root)
    file_menu = Menu(menu_bar, tearoff=0)
    edit_menu = Menu(menu_bar, tearoff=0)
    view_menu = Menu(menu_bar, tearoff=0)
    help_menu = Menu(menu_bar, tearoff=0)
    themes_menu = Menu(menu_bar, tearoff=0)

    # 添加菜单列表
    menu_bar.add_cascade(label='File', menu=file_menu)
    menu_bar.add_cascade(label='Edit', menu=edit_menu)
    menu_bar.add_cascade(label='View', menu=view_menu)
    menu_bar.add_cascade(label='Help', menu=help_menu)

    # File菜单
    imageNew = ImageTk.PhotoImage(Image.open(Path(d) / newFile))
    file_menu.add_command(label='New',
                          accelerator='Ctrl+N',
                          compound='left',
                          image=imageNew,
                          command=lambda: new_file())

    imageOpen = ImageTk.PhotoImage(Image.open(Path(d) / openFile))
    file_menu.add_command(label='Open',
                          accelerator='Ctrl+O',
                          compound='left',
                          image=imageOpen,
                          command=lambda: open_file())

    imageSave = ImageTk.PhotoImage(Image.open(Path(d) / saveFile))
    file_menu.add_command(label='Save',
                          accelerator='Ctrl+S',
                          compound='left',
                          image=imageSave,
                          command=lambda: save())

    file_menu.add_command(label='Save as',
                          accelerator='Ctrl+shift+s',
                          compound='left',
                          image='',
                          command=lambda: save_as())

    file_menu.add_separator()
    file_menu.add_command(label='Exit',
                          accelerator='Alt+F4',
                          compound='left',
                          image='',
                          command=lambda: exitText())

    # Edit菜单
    imageUndo = ImageTk.PhotoImage(Image.open(Path(d) / undoFile))
    edit_menu.add_command(label='Undo',
                          accelerator='Ctrl+Z',
                          compound='left',
                          image=imageUndo,
                          command=lambda: undo())

    imageRedo = ImageTk.PhotoImage(Image.open(Path(d) / undoFile))
    edit_menu.add_command(label='Redo',
                          accelerator='Ctrl+Y',
                          compound='left',
                          image=imageRedo,
                          command=lambda: redo())

    imageCut = ImageTk.PhotoImage(Image.open(Path(d) / cutFile))
    edit_menu.add_command(label='Cut',
                          accelerator='Ctrl+X',
                          compound='left',
                          image=imageCut,
                          command=lambda: cut())

    imageCopy = ImageTk.PhotoImage(Image.open(Path(d) / copyFile))
    edit_menu.add_command(label='Copy',
                          accelerator='Ctrl+C',
                          compound='left',
                          image=imageCopy,
                          command=lambda: copy())

    imagePaste = ImageTk.PhotoImage(Image.open(Path(d) / pasteFile))
    edit_menu.add_command(label='Paste',
                          accelerator='Ctrl+V',
                          compound='left',
                          image=imagePaste,
                          command=lambda: paste())

    edit_menu.add_separator()
    edit_menu.add_command(label='Find',
                          accelerator='Ctrl+F',
                          compound='left',
                          image='',
                          command=lambda: find_text())

    edit_menu.add_separator()
    edit_menu.add_command(label='Select All',
                          accelerator='Ctrl+A',
                          compound='left',
                          image='',
                          command=lambda: select_all())

    # Help菜单
    help_menu.add_command(label='About',
                          accelerator='',
                          compound='left',
                          image='',
                          command=lambda: display_about_messagebox())
    help_menu.add_command(label='Help',
                          accelerator='',
                          compound='left',
                          image='',
                          command=lambda: display_help_messagebox())

    # 添加横向Frame
    shortcut_bar = Frame(root, height=25, background='light seagreen')
    shortcut_bar.pack(expand='no', fill='x')

    # 添加纵向Frame
    line_number_bar = Text(root,
                           width=4,
                           padx=3,
                           takefocus=0,
                           border=0,
                           background='khaki',
                           state='disabled',
                           wrap='none')
    line_number_bar.pack(side='left', fill='y')

    # 添加文本框
    ## undo  True 可以无限撤销 False 不能撤销
    content_text = Text(root, wrap='word', undo=True)

    ## 给文本框绑定鼠标事件   同时绑定执行函数
    content_text.bind('<Control-y>', redo)  # handling Ctrl + small-case y
    content_text.bind('<Control-Y>', redo)  # handling Ctrl + upper-case Y
    content_text.bind('<Control-a>',
                      select_all)  # handling Ctrl + upper-case a
    content_text.bind('<Control-A>',
                      select_all)  # handling Ctrl + upper-case A
    content_text.bind('<Control-f>', find_text)  #ctrl + f
    content_text.bind('<Control-F>', find_text)  #ctrl + F
    content_text.bind('<Control-N>', new_file)  #ctrl + N
    content_text.bind('<Control-n>', new_file)  #ctrl + n
    content_text.bind('<Control-O>', open_file)  #ctrl + O
    content_text.bind('<Control-o>', open_file)  #ctrl + o
    content_text.bind('<Control-S>', save)  #ctrl + S
    content_text.bind('<Control-s>', save)  #ctrl + s
    content_text.bind('<Control-Shift-S>', save_as)  #ctrl + shift + S
    content_text.bind('<Control-Shift-s>', save_as)  #ctrl + sgift + s
    content_text.bind('<KeyPress-F1>', display_help_messagebox)
    content_text.bind('<Any-KeyPress>', on_content_changed)  ## 切换行号
    content_text.bind('<Button-1>', on_content_changed)
    content_text.tag_configure('active_line', background='ivory2')

    # 增加右键功能
    popup_menu = Menu(content_text)
    popup_menu.add_command(label='Cut',
                           compound='left',
                           image=imageCut,
                           command=lambda: cut())
    popup_menu.add_command(label='Copy',
                           compound='left',
                           image=imageCopy,
                           command=lambda: copy())
    popup_menu.add_command(label='Paste',
                           compound='left',
                           image=imagePaste,
                           command=lambda: paste())
    popup_menu.add_command(label='Undo',
                           compound='left',
                           image=imageUndo,
                           command=lambda: undo())
    popup_menu.add_command(label='Redo',
                           compound='left',
                           image=imageRedo,
                           command=lambda: redo())
    popup_menu.add_separator()
    popup_menu.add_command(label='Select All', underline=7, command=select_all)

    ## 文本框绑定右键事件
    content_text.bind('<Button-2>', show_popup_menu)

    ## 显示文本框
    content_text.pack(expand='yes', fill='both')

    ## 增加滚动条
    scroll_bar = Scrollbar(content_text)
    content_text.configure(yscrollcommand=scroll_bar.set)
    scroll_bar.config(command=content_text.yview)
    scroll_bar.pack(side='right', fill='y')

    # views 添加下拉选项
    show_line_number = IntVar()
    show_line_number.set(1)
    view_menu.add_checkbutton(label="Show Line Number",
                              variable=show_line_number,
                              command=update_line_numbers)

    show_cursor_info = IntVar()
    show_cursor_info.set(1)
    view_menu.add_checkbutton(label="Show Cursor Location at Bottom",
                              variable=show_cursor_info,
                              command=show_cursor_info_bar)

    to_highlight_line = BooleanVar()
    to_highlight_line.set(1)
    view_menu.add_checkbutton(label="HighLight Current Line",
                              variable=to_highlight_line,
                              command=toggle_highlight)
    toggle_highlight()

    ## 增加分割线
    view_menu.add_cascade(label="Themes", menu=themes_menu)  # theme

    ## 增加theme菜单
    themes_choices = StringVar()
    themes_choices.set(select_themes)
    for k, themes_choice in color_schemes.items():
        themes_menu.add_radiobutton(label=k,
                                    variable=themes_choices,
                                    command=change_theme,
                                    value=k)

    # 添加设置背景功能
    # bgStart = IntVar()
    # bgStart.set(0)
    # view_menu.add_checkbutton(label="Set Background", variable=bgStart, command=select_background)

    ## 添加快捷图标
    icons = [(newFile, 'new_file'), (openFile, 'open_file'),
             (saveFile, 'save'), (cutFile, 'cut'), (copyFile, 'copy'),
             (pasteFile, 'paste'), (undoFile, 'undo'), (redoFile, 'redo'),
             (findFile, 'find_text')]
    for i, icon in enumerate(icons):
        tool_bar_icon = ImageTk.PhotoImage(Image.open(Path(d) / icon[0]))
        # cmd = eval(icon)
        tool_bar = Button(shortcut_bar,
                          image=tool_bar_icon,
                          command=eval(icon[1]))
        tool_bar.image = tool_bar_icon
        tool_bar.pack(side='left')

    ## 添加底部显示行号
    cursor_info_bar = Label(content_text, text='Line: 1 | Column: 1')
    cursor_info_bar.pack(expand=NO, fill=None, side=RIGHT, anchor='se')

    ## 配置menu
    root.config(menu=menu_bar)

    ## 设置rootname
    root.title(PROGRAM_NAME)

    ## 设置最小size
    root.minsize(800, 600)

    ## 设置居中显示
    root.geometry('%dx%d+%d+%d' % (800, 600,
                                   (root.winfo_screenwidth() - 1000) / 2,
                                   (root.winfo_screenheight() - 600) / 2))

    ## 配置默认主题
    change_theme()

    mainloop()


上一篇:tkinter 应用程序:猜数字

下一篇:tkinter 应用程序:IDE 编辑器 Thonny