cef3和tkinter网页浏览器

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()

相关推荐
青 春 记 忆3 小时前
Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
运维·人工智能·python·docker·容器
GlueNa2SiO33 小时前
03-Flask模板引擎Jinja2详解
笔记·python·flask
临沂GEO5 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
大模型码小白5 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
荷蒲7 小时前
【小白量化Qbuddy】利用AI学习中文Python
人工智能·python·学习
数据狐(Datafox)8 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
量化吞吐机9 小时前
2026年下半年手工交易规则走向量化表达,产品该先接住哪一步
人工智能·python
Metaphor6929 小时前
使用 Python 读取和删除 Excel 文件属性
python·excel
迷迭香yy10 小时前
Python构建转融券多空识别系统从接口到因子的全流程 IG50免费开源股票数据API接口
开发语言·python·开源
月光船幽幽11 小时前
真实即粗糙,光滑是伪造
人工智能·python