以下は『漆黒編纂典蔵』のソースコード全文です:
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter import ttk
from pygments import lex
from pygments.lexers import get_lexer_by_name
from pygments.styles import get_style_by_name
import os
import re
class TextEditor:
def __init__(self, root):
self.root = root
self.root.title("テキストエディタ - 漆黒編纂典蔵")
self.root.geometry("800x600")
self.filename = None
self.language_mode = "python"
self.theme_mode = "dark"
self.wrap_mode = False
self.highlight_zenkaku = False
self.comment_tokens = {
"python": "#",
"javascript": "//",
"html": "<!-- -->",
"c": "//",
"cpp": "//",
"text": "#"
}
self.current_comment_token = self.comment_tokens[self.language_mode]
self._setup_widgets()
self._set_lexer("python")
self.set_dark_theme()
self._configure_tags()
self._update_language_display()
def _setup_widgets(self):
menubar = tk.Menu(self.root)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="開く", command=self.open_file)
file_menu.add_command(label="上書き保存", command=self.save_file)
file_menu.add_command(label="別名で保存", command=self.save_file_as)
menubar.add_cascade(label="ファイル", menu=file_menu)
edit_menu = tk.Menu(menubar, tearoff=0)
edit_menu.add_command(label="コピー", command=self.copy_all_text)
edit_menu.add_command(label="クリア", command=self.clear_text)
edit_menu.add_command(label="コメント", command=self.comment_selection)
edit_menu.add_command(label="アンコメント", command=self.uncomment_selection)
edit_menu.add_command(label="行頭に文字追加", command=self.add_prefix_to_lines)
edit_menu.add_command(label="行頭から文字削除", command=self.remove_prefix_by_length)
menubar.add_cascade(label="編集", menu=edit_menu)
view_menu = tk.Menu(menubar, tearoff=0)
view_menu.add_command(label="ダークモード", command=self.set_dark_theme)
view_menu.add_command(label="ライトモード", command=self.set_light_theme)
view_menu.add_command(label="折り返し ON", command=lambda: self.toggle_wrap(True))
view_menu.add_command(label="折り返し OFF", command=lambda: self.toggle_wrap(False))
self.zenkaku_var = tk.BooleanVar(value=False)
view_menu.add_checkbutton(label="全角ハイライト", command=self.toggle_zenkaku_highlight, variable=self.zenkaku_var)
menubar.add_cascade(label="表示", menu=view_menu)
lang_menu = tk.Menu(menubar, tearoff=0)
for lang in ["python", "javascript", "html", "c", "cpp"]:
lang_menu.add_command(label=lang, command=lambda l=lang: self.change_language(l))
menubar.add_cascade(label="言語モード", menu=lang_menu)
search_menu = tk.Menu(menubar, tearoff=0)
search_menu.add_command(label="検索", command=self.open_search_window)
menubar.add_cascade(label="検索", menu=search_menu)
self.root.config(menu=menubar)
label_frame = tk.Frame(self.root)
label_frame.pack(fill=tk.X)
self.language_label = ttk.Label(label_frame, text="", anchor="e")
self.language_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.wrap_label = ttk.Label(label_frame, text="折り返し: OFF", anchor="e")
self.wrap_label.pack(side=tk.RIGHT)
text_frame = tk.Frame(self.root)
text_frame.pack(fill=tk.BOTH, expand=True)
self.line_numbers = tk.Text(text_frame, width=4, padx=4, takefocus=0, border=0,
background='#2b2b2b', foreground='white', state='disabled')
self.line_numbers.pack(side=tk.LEFT, fill=tk.Y)
self.text = tk.Text(text_frame, undo=True, wrap="none")
self.text.pack(fill=tk.BOTH, expand=1)
scrollbar = tk.Scrollbar(self.text, command=self.on_scrollbar)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.text.config(yscrollcommand=scrollbar.set)
self.text.bind("<KeyRelease>", self.on_key_release)
self.text.bind("<MouseWheel>", self.on_mouse_wheel)
self.find_str = ""
def _set_lexer(self, lang):
try:
self.lexer = get_lexer_by_name(lang)
except:
self.lexer = get_lexer_by_name("text")
def _configure_tags(self):
style_name = "monokai" if self.theme_mode == "dark" else "default"
style = get_style_by_name(style_name)
for token, opts in style:
foreground = opts['color'] if 'color' in opts else None
if foreground:
self.text.tag_configure(str(token), foreground=f"#{foreground}")
def on_key_release(self, event=None):
self.update_line_numbers()
self.highlight_syntax()
def on_scrollbar(self, *args):
self.text.yview(*args)
self.line_numbers.yview(*args)
def on_mouse_wheel(self, event):
self.text.yview_scroll(int(-1*(event.delta/120)), "units")
self.line_numbers.yview_scroll(int(-1*(event.delta/120)), "units")
return "break"
def update_line_numbers(self):
self.line_numbers.config(state='normal')
self.line_numbers.delete('1.0', tk.END)
line_count = int(self.text.index('end-1c').split('.')[0]) + 1
lines = "\n".join(str(i) for i in range(1, line_count + 1))
self.line_numbers.insert('1.0', lines)
self.line_numbers.config(state='disabled')
def highlight_syntax(self):
code = self.text.get("1.0", tk.END)
self.text.mark_set("range_start", "1.0")
for tag in self.text.tag_names():
self.text.tag_remove(tag, "1.0", tk.END)
for token, content in lex(code, self.lexer):
self.text.mark_set("range_end", f"range_start + {len(content)}c")
self.text.tag_add(str(token), "range_start", "range_end")
self.text.mark_set("range_start", "range_end")
if self.highlight_zenkaku:
self.text.tag_remove("zenkaku", "1.0", tk.END)
pattern = r"[\u3000-\u9FFF]"
start = "1.0"
while True:
match = self.text.search(pattern, start, regexp=True, stopindex=tk.END)
if not match:
break
end = f"{match}+1c"
self.text.tag_add("zenkaku", match, end)
start = end
self.text.tag_configure("zenkaku", background="red", foreground="white")
else:
self.text.tag_remove("zenkaku", "1.0", tk.END)
def set_dark_theme(self):
self.theme_mode = "dark"
self.text.configure(bg="#1e1e1e", fg="white", insertbackground="white")
self.line_numbers.configure(bg="#2b2b2b", fg="white")
self._configure_tags()
self.highlight_syntax()
def set_light_theme(self):
self.theme_mode = "light"
self.text.configure(bg="white", fg="black", insertbackground="black")
self.line_numbers.configure(bg="#e0e0e0", fg="black")
self._configure_tags()
self.highlight_syntax()
def toggle_wrap(self, state):
self.wrap_mode = state
self.text.config(wrap="word" if state else "none")
self.wrap_label.config(text="折り返し: ON" if state else "折り返し: OFF")
def toggle_zenkaku_highlight(self):
self.highlight_zenkaku = not self.highlight_zenkaku
self.highlight_syntax()
def open_file(self):
file_path = filedialog.askopenfilename(filetypes=[("All files", "*.*")])
if file_path:
with open(file_path, "r", encoding="utf-8") as file:
self.text.delete("1.0", tk.END)
self.text.insert("1.0", file.read())
self.filename = file_path
self._update_title()
self.highlight_syntax()
def save_file(self):
if not self.filename:
self.save_file_as()
return
with open(self.filename, "w", encoding="utf-8") as file:
file.write(self.text.get("1.0", tk.END))
self._update_title()
def save_file_as(self):
file_path = filedialog.asksaveasfilename(filetypes=[("All files", "*.*")])
if not file_path:
return
self.filename = file_path
self.save_file()
def _update_title(self):
name = os.path.basename(self.filename) if self.filename else "無題"
self.root.title(f"Text Editor - {name}")
def copy_all_text(self):
self.root.clipboard_clear()
self.root.clipboard_append(self.text.get("1.0", tk.END))
def clear_text(self):
self.text.delete("1.0", tk.END)
def change_language(self, lang):
self.language_mode = lang
self._set_lexer(lang)
self.current_comment_token = self.comment_tokens.get(lang, "#")
self._configure_tags()
self._update_language_display()
self.highlight_syntax()
def _update_language_display(self):
self.language_label.config(text=f"現在の言語モード:{self.language_mode}")
def comment_selection(self):
try:
start = self.text.index("sel.first linestart")
end = self.text.index("sel.last lineend")
except tk.TclError:
messagebox.showinfo("情報", "コメントする範囲を選択してください。")
return
lines = self.text.get(start, end).split("\n")
if self.language_mode == "html":
self.text.delete(start, end)
self.text.insert(start, f"<!--\n{chr(10).join(lines)}\n-->")
else:
commented = [self.current_comment_token + " " + line for line in lines]
self.text.delete(start, end)
self.text.insert(start, "\n".join(commented))
def uncomment_selection(self):
try:
start = self.text.index("sel.first linestart")
end = self.text.index("sel.last lineend")
except tk.TclError:
messagebox.showinfo("情報", "アンコメントする範囲を選択してください。")
return
lines = self.text.get(start, end).split("\n")
if self.language_mode == "html":
text = "\n".join(lines)
if text.startswith("<!--") and text.endswith("-->"):
text = text[4:-3].strip("\n")
self.text.delete(start, end)
self.text.insert(start, text)
else:
prefix = self.current_comment_token + " "
uncommented = [line[len(prefix):] if line.startswith(prefix) else line for line in lines]
self.text.delete(start, end)
self.text.insert(start, "\n".join(uncommented))
def add_prefix_to_lines(self):
prefix = self.prompt_user("行頭に追加する文字列を入力してください:")
if not prefix:
return
try:
start = self.text.index("sel.first linestart")
end = self.text.index("sel.last lineend")
except tk.TclError:
messagebox.showinfo("情報", "範囲を選択してください。")
return
lines = self.text.get(start, end).split("\n")
new_lines = [prefix + line for line in lines]
self.text.delete(start, end)
self.text.insert(start, "\n".join(new_lines))
def remove_prefix_by_length(self):
length_str = self.prompt_user("行頭から削除する文字数を入力してください:")
if not length_str or not length_str.isdigit():
return
length = int(length_str)
try:
start = self.text.index("sel.first linestart")
end = self.text.index("sel.last lineend")
except tk.TclError:
messagebox.showinfo("情報", "範囲を選択してください。")
return
lines = self.text.get(start, end).split("\n")
new_lines = [line[length:] if len(line) >= length else '' for line in lines]
self.text.delete(start, end)
self.text.insert(start, "\n".join(new_lines))
def prompt_user(self, message):
popup = tk.Toplevel(self.root)
popup.title("入力")
label = tk.Label(popup, text=message)
label.pack(padx=10, pady=5)
entry = tk.Entry(popup)
entry.pack(padx=10, pady=5)
entry.focus_set()
result = {}
def on_submit(event=None):
result["text"] = entry.get()
popup.destroy()
entry.bind("<Return>", on_submit)
button = tk.Button(popup, text="OK", command=on_submit)
button.pack(pady=5)
self.root.wait_window(popup)
return result.get("text", "")
def open_search_window(self):
search_window = tk.Toplevel(self.root)
search_window.title("検索")
def on_close():
self.text.tag_remove("highlight", "1.0", tk.END)
search_window.destroy()
search_window.protocol("WM_DELETE_WINDOW", on_close)
search_label = ttk.Label(search_window, text="検索する文字列:")
search_label.pack(side=tk.LEFT, padx=10)
search_entry = ttk.Entry(search_window)
search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
search_entry.focus_set()
def on_search_button():
self.on_search(search_entry.get())
search_entry.bind("<Return>", lambda event: on_search_button())
search_button = tk.Button(search_window, text="検索", command=on_search_button)
search_button.pack(side=tk.RIGHT)
def on_search(self, search_str):
self.find_str = search_str
self.highlight_search_results()
def highlight_search_results(self):
self.text.tag_remove("highlight", "1.0", tk.END)
pattern = self.find_str
start_pos = "1.0"
while True:
start_pos = self.text.search(pattern, start_pos, nocase=True, stopindex=tk.END)
if not start_pos:
break
end_pos = f"{start_pos}+{len(self.find_str)}c"
self.text.tag_add("highlight", start_pos, end_pos)
start_pos = end_pos
self.text.tag_configure("highlight", background="yellow")
if __name__ == "__main__":
root = tk.Tk()
app = TextEditor(root)
root.mainloop()