
import os
import sys
import json
import time
import subprocess
import traceback
import tkinter as tk
from tkinter import ttk, messagebox
try:
from cefpython3 import cefpython as cef
except ImportError:
raise SystemExit('请先安装 cefpython3:pip install cefpython3')
APP_DIR = os.path.dirname(os.path.abspath(file))
BOOKMARK_FILE = os.path.join(APP_DIR, 'openlist.json')
SESSION_FILE = os.path.join(APP_DIR, 'session.json')
ERROR_LOG = os.path.join(APP_DIR, 'error.log')
DEFAULT_URL = 'https://www.baidu.com/'
WINDOW_WIDTH = 1200
WINDOW_HEIGHT = 800
DEFAULT_BOOKMARKS = {
'百度一下': 'https://www.baidu.com/',
'必应搜索': 'https://cn.bing.com/'
}
def global_exception_handler(exc_type, exc_value, exc_traceback):
try:
with open(ERROR_LOG, 'a', encoding='utf-8') as f:
f.write(f"\n{time.strftime('%Y-%m-%d %H:%M:%S')}\n")
traceback.print_exception(exc_type, exc_value, exc_traceback, file=f)
except Exception:
pass
sys.exit(1)
sys.excepthook = global_exception_handler
def normalize_url(text):
text = (text or '').strip()
if not text:
return ''
if text.startswith(('http://', 'https://', 'ftp://', 'file://', 'about:')):
return text
if text.startswith('www.'):
return 'http://' + text
return 'https://' + text
def is_allowed_url(url):
url = (url or '').strip()
if not url:
return False
return url.startswith(('http://', 'https://', 'ftp://', 'file://'))
def get_browser_id(browser):
if browser is None:
return None
try:
return browser.GetIdentifier()
except Exception:
return id(browser)
def set_browser_bounds(browser, x, y, width, height):
if not browser:
return
try:
browser.SetBounds(x, y, width, height)
except Exception:
pass
class UrlDialog(tk.Toplevel):
def init(self, parent):
super().init(parent)
self.title('输入网址')
self.transient(parent)
self.grab_set()
self.resizable(True, False)
self.result = None
box = ttk.Frame(self, padding=15)
box.pack(fill='both', expand=True)
ttk.Label(box, text='网址:').grid(row=0, column=0, sticky='e', padx=5, pady=8)
self.url_var = tk.StringVar(value='https://')
entry = ttk.Entry(box, textvariable=self.url_var, width=70)
entry.grid(row=0, column=1, pady=8)
btns = ttk.Frame(box)
btns.grid(row=1, column=0, columnspan=2, pady=10)
ttk.Button(btns, text='访问', command=self.ok).pack(side='left', padx=8)
ttk.Button(btns, text='取消', command=self.cancel).pack(side='left', padx=8)
entry.focus_set()
self.bind('<Return>', lambda e: self.ok())
self.bind('<Escape>', lambda e: self.cancel())
self.update_idletasks()
def ok(self):
url = normalize_url(self.url_var.get())
if not url:
messagebox.showwarning('提示', '请输入有效网址', parent=self)
return
self.result = url
self.destroy()
def cancel(self):
self.destroy()
class BookmarkDialog(tk.Toplevel):
def init(self, parent, title='收藏夹', name='', url=''):
super().init(parent)
self.title(title)
self.transient(parent)
self.grab_set()
self.resizable(True, False)
self.result = None
box = ttk.Frame(self, padding=15)
box.pack(fill='both', expand=True)
ttk.Label(box, text='名称:').grid(row=0, column=0, sticky='e', padx=5, pady=8)
self.name_var = tk.StringVar(value=name)
name_entry = ttk.Entry(box, textvariable=self.name_var, width=35)
name_entry.grid(row=0, column=1, pady=8)
ttk.Label(box, text='网址:').grid(row=1, column=0, sticky='e', padx=5, pady=8)
self.url_var = tk.StringVar(value=url)
url_entry = ttk.Entry(box, textvariable=self.url_var, width=70)
url_entry.grid(row=1, column=1, pady=8)
btns = ttk.Frame(box)
btns.grid(row=2, column=0, columnspan=2, pady=10)
ttk.Button(btns, text='确定', command=self.ok).pack(side='left', padx=8)
ttk.Button(btns, text='取消', command=self.cancel).pack(side='left', padx=8)
name_entry.focus_set()
self.bind('<Return>', lambda e: self.ok())
self.bind('<Escape>', lambda e: self.cancel())
self.update_idletasks()
def ok(self):
name = self.name_var.get().strip()
url = normalize_url(self.url_var.get())
if not name:
messagebox.showwarning('提示', '请输入收藏名称', parent=self)
return
if not url:
messagebox.showwarning('提示', '请输入有效网址', parent=self)
return
self.result = (name, url)
self.destroy()
def cancel(self):
self.destroy()
class Tab(object):
def init(self, notebook, url, app):
self.app = app
self.frame = ttk.Frame(notebook)
self.header = tk.Frame(self.frame, bg='#f0f0f0', height=32)
self.header.pack(fill='x')
self.header.pack_propagate(False)
self.title_label = tk.Label(self.header, text='新标签', bg='#f0f0f0', font=('Microsoft YaHei UI', 10),
anchor='w')
self.title_label.pack(side='left', fill='x', expand=True, padx=10, pady=6)
self.close_btn = tk.Button(self.header, text='X', command=self.close_tab, width=2, bg='#ffcccc', relief='flat',
font=('Arial', 10, 'bold'))
self.close_btn.pack(side='right', padx=8, pady=6)
self.browser_frame = tk.Frame(self.frame, bg='white')
self.browser_frame.pack(fill='both', expand=True)
self.url = url
self.title = '新标签'
self.browser = None
self.browser_id = None
self.closed = False
def close_tab(self):
self.app.close_tab(self)
def set_title(self, title):
if not title:
title = self.url or '新标签'
self.title = title
display_text = title if len(title) <= 20 else title:20 + '...'
self.title_label.config(text=display_text)
class CefClientHandler(object):
def init(self, app):
self.app = app
def call_main(self, func):
try:
self.app.root.after(0, func)
except Exception:
pass
def OnTitleChange(self, *args):
try:
browser = args0
title = args1 if len(args) > 1 else ''
if not isinstance(title, str):
title = str(title)
browser_id = get_browser_id(browser)
self.call_main(lambda b=browser_id, t=title: self.app.update_title(b, t))
except Exception:
pass
def OnAddressChange(self, *args):
try:
browser = args0
frame = args1 if len(args) > 1 else None
url = args2 if len(args) > 2 else ''
if frame and frame.IsMain() and url:
browser_id = get_browser_id(browser)
self.call_main(lambda b=browser_id, u=url: self.app.update_address(b, u))
except Exception:
pass
def OnLoadEnd(self, *args):
try:
browser = args0 if len(args) > 0 else None
frame = args1 if len(args) > 1 else None
self.app.inject_link_hook(browser, frame)
except Exception:
pass
def OnBeforePopup(self, *args):
try:
url = ''
if len(args) >= 3 and isinstance(args2, str):
url = args2
else:
for item in args:
if isinstance(item, str) and item.strip():
url = item
break
url = normalize_url(url)
if url and is_allowed_url(url) and url != 'about:blank':
self.call_main(lambda u=url: self.app.schedule_new_tab(u))
except Exception:
pass
return True
def OnBeforeBrowse(self, *args):
try:
browser = args0
frame = args1
request = args2
if frame and frame.IsMain() and request:
transition = request.GetTransitionType()
try:
tt_link = cef.TT_LINK
except AttributeError:
tt_link = 2
if transition == tt_link:
url = request.GetUrl()
if url and is_allowed_url(url):
if '#' in url:
current_url = browser.GetUrl() if browser else ''
if url.split('#', 1)0 == current_url.split('#', 1)0:
return False
self.call_main(lambda u=url: self.app.schedule_new_tab(u))
return True
except Exception:
pass
return False
class App(object):
def init(self, root):
self.root = root
self.all_tabs = \[\]
self.tabs = {}
self.recent_opens = {}
self.bookmarks = {}
self.closing = False
self.shutdown_done = False
self._shutdown_wait_count = 0
self.load_bookmarks()
self.build_ui()
self.refresh_bookmarks()
self.root.protocol('WM_DELETE_WINDOW', self.on_close_window)
self.root.after(10, self.pump_cef)
if not self.restore_session():
self.add_tab(DEFAULT_URL)
def build_ui(self):
self.root.title('Rogabet 多标签浏览器')
self.root.geometry('{}x{}'.format(WINDOW_WIDTH, WINDOW_HEIGHT))
self.root.minsize(800, 600)
menu_bar = tk.Menu(self.root)
file_menu = tk.Menu(menu_bar, tearoff=0)
file_menu.add_command(label='关闭浏览器', command=self.quit_app)
menu_bar.add_cascade(label='文件', menu=file_menu)
self.root.config(menu=menu_bar)
try:
style = ttk.Style(self.root)
style.configure('.', font=('Microsoft YaHei UI', 11))
except Exception:
pass
top = ttk.Frame(self.root, padding=6)
top.pack(side='top', fill='x')
ttk.Button(top, text='输入网址', command=self.show_url_dialog).pack(side='left', padx=4)
ttk.Button(top, text='新标签', command=lambda: self.add_tab(DEFAULT_URL)).pack(side='left', padx=4)
self.address_var = tk.StringVar()
address_entry = ttk.Entry(top, textvariable=self.address_var, width=55)
address_entry.pack(side='left', fill='x', expand=True, padx=8)
address_entry.bind('<Return>', lambda e: self.open_address_entry())
ttk.Button(top, text='在新标签访问', command=self.open_address_entry).pack(side='left', padx=4)
main = ttk.Frame(self.root, padding=4)
main.pack(fill='both', expand=True)
left = ttk.LabelFrame(main, text='收藏夹', padding=6)
left.pack(side='left', fill='y')
left.config(width=250)
left.pack_propagate(False)
list_bar = ttk.Frame(left)
list_bar.pack(fill='both', expand=True)
self.bookmark_listbox = tk.Listbox(list_bar, font=('Microsoft YaHei UI', 12), activestyle='dotbox')
yscroll = ttk.Scrollbar(list_bar, orient='vertical', command=self.bookmark_listbox.yview)
self.bookmark_listbox.configure(yscrollcommand=yscroll.set)
yscroll.pack(side='right', fill='y')
self.bookmark_listbox.pack(side='left', fill='both', expand=True)
self.bookmark_listbox.bind('<Double-1>', lambda e: self.open_selected_bookmark())
btn_bar = ttk.Frame(left)
btn_bar.pack(fill='x', pady=(8, 0))
buttons = [
('打开', self.open_selected_bookmark),
('新增', self.add_bookmark),
('修改', self.edit_bookmark),
('删除', self.delete_bookmark),
('刷新', self.refresh_bookmarks),
('保存', lambda: self.save_bookmarks(True)),
]
for index, item in enumerate(buttons):
text, command = item
row = index // 2
col = index % 2
ttk.Button(btn_bar, text=text, command=command).grid(row=row, column=col, sticky='ew', padx=3, pady=3)
btn_bar.columnconfigure(0, weight=1)
btn_bar.columnconfigure(1, weight=1)
right = ttk.Frame(main)
right.pack(side='left', fill='both', expand=True)
self.notebook = ttk.Notebook(right)
self.notebook.pack(fill='both', expand=True)
self.notebook.bind('<<NotebookTabChanged>>', self.on_tab_changed)
def on_close_window(self):
pass
def pump_cef(self):
if self.closing or self.shutdown_done:
return
try:
if hasattr(cef, 'MessageLoopWork'):
cef.MessageLoopWork()
except Exception:
pass
self.root.after(10, self.pump_cef)
def load_bookmarks(self):
if os.path.exists(BOOKMARK_FILE):
try:
with open(BOOKMARK_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, dict):
self.bookmarks = data
else:
self.bookmarks = DEFAULT_BOOKMARKS.copy()
except Exception:
self.bookmarks = DEFAULT_BOOKMARKS.copy()
else:
self.bookmarks = DEFAULT_BOOKMARKS.copy()
self.save_bookmarks(False)
def save_bookmarks(self, show_message=False):
try:
with open(BOOKMARK_FILE, 'w', encoding='utf-8') as f:
json.dump(self.bookmarks, f, ensure_ascii=False, indent=2)
if show_message:
messagebox.showinfo('提示', '收藏夹已保存到 openlist.json')
except Exception as error:
messagebox.showerror('保存失败', str(error))
def refresh_bookmarks(self):
self.bookmark_listbox.delete(0, tk.END)
for name in self.bookmarks.keys():
self.bookmark_listbox.insert(tk.END, name)
def open_selected_bookmark(self):
selection = self.bookmark_listbox.curselection()
if not selection:
messagebox.showinfo('提示', '请先选择一个收藏项')
return
name = self.bookmark_listbox.get(selection0)
url = self.bookmarks.get(name, '')
if url:
self.add_tab(url)
def add_bookmark(self):
dlg = BookmarkDialog(self.root, '新增收藏')
self.root.wait_window(dlg)
if not dlg.result:
return
name, url = dlg.result
if name in self.bookmarks:
if not messagebox.askyesno('提示', '收藏名称已存在,是否覆盖?'):
return
self.bookmarksname = url
self.save_bookmarks(False)
self.refresh_bookmarks()
def edit_bookmark(self):
selection = self.bookmark_listbox.curselection()
if not selection:
messagebox.showinfo('提示', '请先选择要修改的收藏项')
return
old_name = self.bookmark_listbox.get(selection0)
old_url = self.bookmarks.get(old_name, '')
dlg = BookmarkDialog(self.root, '修改收藏', old_name, old_url)
self.root.wait_window(dlg)
if not dlg.result:
return
name, url = dlg.result
if name != old_name:
if name in self.bookmarks:
if not messagebox.askyesno('提示', '收藏名称已存在,是否覆盖?'):
return
del self.bookmarksold_name
self.bookmarksname = url
self.save_bookmarks(False)
self.refresh_bookmarks()
def delete_bookmark(self):
selection = self.bookmark_listbox.curselection()
if not selection:
messagebox.showinfo('提示', '请先选择要删除的收藏项')
return
name = self.bookmark_listbox.get(selection0)
if messagebox.askyesno('提示', '确定删除收藏:{} ?'.format(name)):
if name in self.bookmarks:
del self.bookmarksname
self.save_bookmarks(False)
self.refresh_bookmarks()
def show_url_dialog(self):
dlg = UrlDialog(self.root)
self.root.wait_window(dlg)
if dlg.result:
self.add_tab(dlg.result)
def open_address_entry(self):
url = normalize_url(self.address_var.get())
if not url:
messagebox.showinfo('提示', '请输入网址')
return
self.add_tab(url)
self.address_var.set('')
def schedule_new_tab(self, url, source=''):
if self.closing:
return
url = normalize_url(url)
if not url or url == 'about:blank':
return
if not is_allowed_url(url):
return
now = time.time()
old = self.recent_opens.get(url)
if old is not None and now - old < 1.0:
return
self.recent_opensurl = now
if len(self.recent_opens) > 200:
self.recent_opens.clear()
self.add_tab(url)
def add_tab(self, url=''):
if self.closing:
return None
url = normalize_url(url) or DEFAULT_URL
tab = Tab(self.notebook, url, self)
self.all_tabs.append(tab)
self.notebook.add(tab.frame, text=' ', sticky='nsew')
self.notebook.select(tab.frame)
self.address_var.set(url)
self.root.update_idletasks()
self.hide_other_browsers(tab)
self.save_session()
self.root.after(80, lambda: self.create_browser_for_tab(tab))
return tab
def create_browser_for_tab(self, tab):
if self.closing or tab.closed or tab.browser is not None:
return
tab.browser_frame.update_idletasks()
self.root.update_idletasks()
width = tab.browser_frame.winfo_width()
height = tab.browser_frame.winfo_height()
if width <= 1:
width = self.notebook.winfo_width()
if height <= 1:
height = self.notebook.winfo_height()
if width <= 1:
width = 1200
if height <= 1:
height = 800
window_info = cef.WindowInfo()
frame_hwnd = tab.browser_frame.winfo_id()
try:
window_info.SetAsChild(frame_hwnd, 0, 0, width, height)
except Exception:
messagebox.showerror('错误', '创建浏览器窗口失败:SetAsChild 不可用')
self.close_tab(tab)
return
try:
tab.browser = cef.CreateBrowserSync(window_info=window_info, url=tab.url)
except Exception as error:
messagebox.showerror('错误', '创建浏览器失败:{}'.format(str(error)))
self.close_tab(tab)
return
tab.browser_id = get_browser_id(tab.browser)
self.tabstab.browser_id = tab
tab.browser_frame.bind('<Configure>', lambda e, t=tab: self.on_browser_frame_configure(t))
self.hide_other_browsers(tab)
self.show_browser(tab)
self.focus_tab(tab)
tab.set_title(tab.title)
def on_browser_frame_configure(self, tab):
if self.closing or tab.closed or not tab.browser:
return
try:
if self.notebook.select() != str(tab.frame):
return
except Exception:
pass
self.resize_tab(tab)
def resize_tab(self, tab):
if not tab.browser:
return
width = tab.browser_frame.winfo_width()
height = tab.browser_frame.winfo_height()
if width <= 1 or height <= 1:
return
set_browser_bounds(tab.browser, 0, 0, width, height)
def focus_tab(self, tab):
if tab.browser:
try:
tab.browser.SetFocus(True)
except Exception:
pass
def hide_browser(self, tab):
if not tab.browser or tab.closed:
return
try:
tab.browser.SetBounds(-10000, -10000, 1, 1)
except Exception:
pass
def show_browser(self, tab):
if not tab.browser or tab.closed:
return
try:
self.resize_tab(tab)
except Exception:
pass
def hide_other_browsers(self, current_tab):
for tab in self.all_tabs:
if tab.closed:
continue
if current_tab is not None and tab is current_tab:
continue
self.hide_browser(tab)
def on_tab_changed(self, event=None):
selected = None
try:
selected = self.notebook.select()
except Exception:
selected = None
if not selected:
return
current = None
for tab in self.all_tabs:
if tab.closed:
continue
if str(tab.frame) == selected:
current = tab
break
if not current:
return
self.root.update_idletasks()
self.hide_other_browsers(current)
self.show_browser(current)
self.focus_tab(current)
if current.browser:
try:
self.address_var.set(current.browser.GetUrl())
except Exception:
self.address_var.set(current.url)
else:
self.address_var.set(current.url)
def update_title(self, browser_id, title):
tab = self.tabs.get(browser_id)
if not tab or tab.closed:
return
tab.set_title(title)
def update_address(self, browser_id, url):
tab = self.tabs.get(browser_id)
if not tab or tab.closed:
return
tab.url = url
try:
selected_frame = self.notebook.select()
if str(tab.frame) == selected_frame:
self.address_var.set(url)
except Exception:
pass
def inject_link_hook(self, browser, frame):
pass
def close_tab(self, tab):
if tab.closed:
return
tab.closed = True
try:
self.hide_browser(tab)
except Exception:
pass
try:
if tab.browser:
tab.browser.CloseBrowser(True)
tab.browser = None
except Exception:
pass
if tab.browser_id in self.tabs:
del self.tabstab.browser_id
try:
self.notebook.forget(tab.frame)
except Exception:
pass
if tab in self.all_tabs:
self.all_tabs.remove(tab)
self.save_session()
def save_session(self):
urls = \[\]
for tab in self.all_tabs:
if not tab.closed:
url = tab.url
if tab.browser:
try:
current_url = tab.browser.GetUrl()
if current_url and is_allowed_url(current_url):
url = current_url
except Exception:
pass
if url:
urls.append(url)
try:
with open(SESSION_FILE, 'w', encoding='utf-8') as f:
json.dump(urls, f, ensure_ascii=False)
except Exception:
pass
def restore_session(self):
if os.path.exists(SESSION_FILE):
try:
with open(SESSION_FILE, 'r', encoding='utf-8') as f:
urls = json.load(f)
if isinstance(urls, list) and urls:
for url in urls:
self.add_tab(url)
return True
except Exception:
pass
return False
def quit_app(self):
if self.shutdown_done:
return
self.shutdown_done = True
try:
cef.Shutdown()
except Exception:
pass
self.root.destroy()
sys.exit(0)
def end_programr2(pro_name):
os.system('%s%s' % ("taskkill /F /IM ", pro_name))
end_programr2("cefpython3web.exe")
if self.closing:
return
self.closing = True
for tab in list(self.all_tabs):
try:
self.hide_browser(tab)
except Exception:
pass
try:
if tab.browser:
tab.browser.CloseBrowser(True)
tab.browser = None
except Exception:
pass
self.root.after(10, self.wait_for_shutdown)
def wait_for_shutdown(self):
if self.shutdown_done:
return
self._shutdown_wait_count += 1
try:
if hasattr(cef, 'MessageLoopWork'):
cef.MessageLoopWork()
except Exception:
pass
if self._shutdown_wait_count < 50:
self.root.after(10, self.wait_for_shutdown)
else:
self.final_shutdown()
def final_shutdown(self):
if self.shutdown_done:
return
self.shutdown_done = True
try:
cef.Shutdown()
except Exception:
pass
try:
self.root.destroy()
except Exception:
pass
sys.exit(0)
def run_browser():
try:
cef.DpiAware.SetProcessDpiAware()
except Exception:
pass
settings = {
"cache_path": os.path.join(APP_DIR, "cef_cache"),
"persist_session_cookies": True,
}
switches = {
"disable-gpu": "",
"disable-gpu-compositing": "",
"disable-gpu-sandbox": "",
"no-sandbox": "",
}
cef.Initialize(settings=settings, switches=switches)
root = tk.Tk()
app = App(root)
handler = CefClientHandler(app)
try:
cef.SetClientHandler(handler)
except Exception:
pass
root.mainloop()
def main():
if '--child' not in sys.argv:
while True:
cmd = sys.executable, os.path.abspath(__file__), '--child'
result = subprocess.run(cmd)
if result.returncode == 0:
break
time.sleep(1)
else:
run_browser()
if name == 'main':
main()