# An example of embedding CEF browser in wxPython on Linux. # Tested with wxPython 2.8.12.1 (gtk2-unicode). # To install wxPython type "sudo apt-get install python-wxtools". # This example implements a custom "_OnResourceResponse" callback # that emulates reading response by utilizing Resourcehandler # and WebRequest. FIX_ENCODING_BUG = True BROWSER_DEFAULT_ENCODING = "utf-8" # The official CEF Python binaries come with tcmalloc hook # disabled. But if you've built custom binaries and kept tcmalloc # hook enabled, then be aware that in such case it is required # for the cefpython module to be the very first import in # python scripts. See Issue 73 in the CEF Python Issue Tracker # for more details. import ctypes, os, sys libcef_so = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'libcef.so') if os.path.exists(libcef_so): # Import local module ctypes.CDLL(libcef_so, ctypes.RTLD_GLOBAL) if 0x02070000 <= sys.hexversion < 0x03000000: import cefpython_py27 as cefpython else: raise Exception("Unsupported python version: %s" % sys.version) else: # Import from package from cefpython3 import cefpython import wx import time import re import uuid import platform import shutil class ClientHandler: # RequestHandler.GetResourceHandler() def GetResourceHandler(self, browser, frame, request): # Called on the IO thread before a resource is loaded. # To allow the resource to load normally return None. print("GetResourceHandler(): url = %s" % request.GetUrl()) resHandler = ResourceHandler() resHandler._clientHandler = self resHandler._browser = browser resHandler._frame = frame resHandler._request = request self._AddStrongReference(resHandler) return resHandler def _OnResourceResponse(self, browser, frame, request, requestStatus, requestError, response, data): # This callback is emulated through ResourceHandler # and WebRequest. Real "OnResourceResponse" is not yet # available in CEF 3 (as of CEF revision 1450). See # issue 515 in the CEF Issue Tracker: # https://code.google.com/p/chromiumembedded/issues/detail?id=515 # ---- # requestStatus => cefpython.WebRequest.Status # {"Unknown", "Success", "Pending", "Canceled", "Failed"} # For "file://" requests the status will be "Unknown". # requestError => see the NetworkError wiki page # response.GetStatus() => http status code print("_OnResourceResponse()") print("data length = %s" % len(data)) # Return the new data - you can modify it. if request.GetUrl().startswith("file://") \ and request.GetUrl().endswith("example.html"): data = "This text was inserted through " \ + "_OnResourceResponse()
" + data # Non-english characters are not displaying correctly. # This is a bug in CEF. A quick fix is to get the charset # from response headers and insert into # the html page. # Bug reported on the CEF C++ Forum: # http://www.magpcss.org/ceforum/viewtopic.php?p=18401#p18401 if FIX_ENCODING_BUG: contentType = response.GetHeader("Content-Type") if contentType: contentType = contentType.lower() isHtml = False headerCharset = "" if contentType and "text/html" in contentType: isHtml = True if contentType and "charset" in contentType: match = re.search(r"charset\s*=\s*([^\s]+)", contentType) if match and match.group(1): headerCharset = match.group(1).lower() if isHtml and headerCharset \ and headerCharset != BROWSER_DEFAULT_ENCODING.lower(): if not re.search(r"]+charset\s*=", data, \ re.IGNORECASE): # Only apply the fix if there is no # available on a page. data = ("" % headerCharset) + data return data # A strong reference to ResourceHandler must be kept # during the request. Some helper functions for that. # 1. Add reference in GetResourceHandler() # 2. Release reference in ResourceHandler.ReadResponse() # after request is completed. _resourceHandlers = {} _resourceHandlerMaxId = 0 def _AddStrongReference(self, resHandler): self._resourceHandlerMaxId += 1 resHandler._resourceHandlerId = self._resourceHandlerMaxId self._resourceHandlers[resHandler._resourceHandlerId] = resHandler def _ReleaseStrongReference(self, resHandler): if resHandler._resourceHandlerId in self._resourceHandlers: del self._resourceHandlers[resHandler._resourceHandlerId] else: print("_ReleaseStrongReference() FAILED: resource handler " \ "not found, id = %s" % (resHandler._resourceHandlerId)) class ResourceHandler: # The methods of this class will always be called # on the IO thread. _resourceHandlerId = None _clientHandler = None _browser = None _frame = None _request = None _responseHeadersReadyCallback = None _webRequest = None _webRequestClient = None _offsetRead = 0 def ProcessRequest(self, request, callback): print("ProcessRequest()") # 1. Start the request using WebRequest # 2. Return True to handle the request # 3. Once response headers are ready call # callback.Continue() self._responseHeadersReadyCallback = callback self._webRequestClient = WebRequestClient() self._webRequestClient._resourceHandler = self # Need to set AllowCacheCredentials and AllowCookies for # the cookies to work during POST requests (Issue 127). # To skip cache set the SkipCache request flag. request.SetFlags(cefpython.Request.Flags["AllowCachedCredentials"]\ | cefpython.Request.Flags["AllowCookies"]) # A strong reference to the WebRequest object must kept. self._webRequest = cefpython.WebRequest.Create( request, self._webRequestClient) return True def GetResponseHeaders(self, response, responseLengthOut, redirectUrlOut): print("GetResponseHeaders()") # 1. If the response length is not known set # responseLengthOut[0] to -1 and ReadResponse() # will be called until it returns False. # 2. If the response length is known set # responseLengthOut[0] to a positive value # and ReadResponse() will be called until it # returns False or the specified number of bytes # have been read. # 3. Use the |response| object to set the mime type, # http status code and other optional header values. # 4. To redirect the request to a new URL set # redirectUrlOut[0] to the new url. assert self._webRequestClient._response, "Response object empty" wrcResponse = self._webRequestClient._response response.SetStatus(wrcResponse.GetStatus()) response.SetStatusText(wrcResponse.GetStatusText()) response.SetMimeType(wrcResponse.GetMimeType()) if wrcResponse.GetHeaderMultimap(): response.SetHeaderMultimap(wrcResponse.GetHeaderMultimap()) print("headers: ") print(wrcResponse.GetHeaderMap()) responseLengthOut[0] = self._webRequestClient._dataLength if not responseLengthOut[0]: # Probably a cached page? Or a redirect? pass def ReadResponse(self, dataOut, bytesToRead, bytesReadOut, callback): # print("ReadResponse()") # 1. If data is available immediately copy up to # bytesToRead bytes into dataOut[0], set # bytesReadOut[0] to the number of bytes copied, # and return true. # 2. To read the data at a later time set # bytesReadOut[0] to 0, return true and call # callback.Continue() when the data is available. # 3. To indicate response completion return false. if self._offsetRead < self._webRequestClient._dataLength: dataChunk = self._webRequestClient._data[\ self._offsetRead:(self._offsetRead + bytesToRead)] self._offsetRead += len(dataChunk) dataOut[0] = dataChunk bytesReadOut[0] = len(dataChunk) return True self._clientHandler._ReleaseStrongReference(self) print("no more data, return False") return False def CanGetCookie(self, cookie): # Return true if the specified cookie can be sent # with the request or false otherwise. If false # is returned for any cookie then no cookies will # be sent with the request. return True def CanSetCookie(self, cookie): # Return true if the specified cookie returned # with the response can be set or false otherwise. return True def Cancel(self): # Request processing has been canceled. pass class WebRequestClient: _resourceHandler = None _data = "" _dataLength = -1 _response = None def OnUploadProgress(self, webRequest, current, total): pass def OnDownloadProgress(self, webRequest, current, total): pass def OnDownloadData(self, webRequest, data): # print("OnDownloadData()") self._data += data def OnRequestComplete(self, webRequest): print("OnRequestComplete()") # cefpython.WebRequest.Status = {"Unknown", "Success", # "Pending", "Canceled", "Failed"} statusText = "Unknown" if webRequest.GetRequestStatus() in cefpython.WebRequest.Status: statusText = cefpython.WebRequest.Status[\ webRequest.GetRequestStatus()] print("status = %s" % statusText) print("error code = %s" % webRequest.GetRequestError()) # Emulate OnResourceResponse() in ClientHandler: self._response = webRequest.GetResponse() # Are webRequest.GetRequest() and # self._resourceHandler._request the same? What if # there was a redirect, what will GetUrl() return # for both of them? self._data = self._resourceHandler._clientHandler._OnResourceResponse( self._resourceHandler._browser, self._resourceHandler._frame, webRequest.GetRequest(), webRequest.GetRequestStatus(), webRequest.GetRequestError(), webRequest.GetResponse(), self._data) self._dataLength = len(self._data) # ResourceHandler.GetResponseHeaders() will get called # after _responseHeadersReadyCallback.Continue() is called. self._resourceHandler._responseHeadersReadyCallback.Continue() # Which method to use for message loop processing. # EVT_IDLE - wx application has priority (default) # EVT_TIMER - cef browser has priority # It seems that Flash content behaves better when using a timer. # IMPORTANT! On Linux EVT_IDLE does not work, the events seems to # be propagated only when you move your mouse, which is not the # expected behavior, it is recommended to use EVT_TIMER on Linux, # so set this value to False. USE_EVT_IDLE = False def GetApplicationPath(file=None): import re, os, platform # On Windows after downloading file and calling Browser.GoForward(), # current working directory is set to %UserProfile%. # Calling os.path.dirname(os.path.realpath(__file__)) # returns for eg. "C:\Users\user\Downloads". A solution # is to cache path on first call. if not hasattr(GetApplicationPath, "dir"): if hasattr(sys, "frozen"): dir = os.path.dirname(sys.executable) elif "__file__" in globals(): dir = os.path.dirname(os.path.realpath(__file__)) else: dir = os.getcwd() GetApplicationPath.dir = dir # If file is None return current directory without trailing slash. if file is None: file = "" # Only when relative path. if not file.startswith("/") and not file.startswith("\\") and ( not re.search(r"^[\w-]+:", file)): path = GetApplicationPath.dir + os.sep + file if platform.system() == "Windows": path = re.sub(r"[/\\]+", re.escape(os.sep), path) path = re.sub(r"[/\\]+$", "", path) return path return str(file) def ExceptHook(excType, excValue, traceObject): import traceback, os, time, codecs # This hook does the following: in case of exception write it to # the "error.log" file, display it to the console, shutdown CEF # and exit application immediately by ignoring "finally" (os._exit()). errorMsg = "\n".join(traceback.format_exception(excType, excValue, traceObject)) errorFile = GetApplicationPath("error.log") try: appEncoding = cefpython.g_applicationSettings["string_encoding"] except: appEncoding = "utf-8" if type(errorMsg) == bytes: errorMsg = errorMsg.decode(encoding=appEncoding, errors="replace") try: with codecs.open(errorFile, mode="a", encoding=appEncoding) as fp: fp.write("\n[%s] %s\n" % ( time.strftime("%Y-%m-%d %H:%M:%S"), errorMsg)) except: print("cefpython: WARNING: failed writing to error file: %s" % ( errorFile)) # Convert error message to ascii before printing, otherwise # you may get error like this: # | UnicodeEncodeError: 'charmap' codec can't encode characters errorMsg = errorMsg.encode("ascii", errors="replace") errorMsg = errorMsg.decode("ascii", errors="replace") print("\n"+errorMsg+"\n") cefpython.QuitMessageLoop() cefpython.Shutdown() os._exit(1) class MainFrame(wx.Frame): browser = None mainPanel = None def __init__(self): wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='wxPython CEF 3 example', size=(800,600)) self.CreateMenu() # Cannot attach browser to the main frame as this will cause # the menu not to work. # -- # You also have to set the wx.WANTS_CHARS style for # all parent panels/controls, if it's deeply embedded. self.mainPanel = wx.Panel(self, style=wx.WANTS_CHARS) windowInfo = cefpython.WindowInfo() windowInfo.SetAsChild(self.mainPanel.GetGtkWidget()) # Linux requires adding "file://" for local files, # otherwise /home/some will be replaced as http://home/some self.browser = cefpython.CreateBrowserSync( windowInfo, # If there are problems with Flash you can disable it here, # by disabling all plugins. browserSettings={"plugins_disabled": False, "default_encoding": BROWSER_DEFAULT_ENCODING}, navigateUrl="file://"+GetApplicationPath("example.html")) clientHandler = ClientHandler() self.browser.SetClientHandler(clientHandler) self.Bind(wx.EVT_CLOSE, self.OnClose) if USE_EVT_IDLE: # Bind EVT_IDLE only for the main application frame. self.Bind(wx.EVT_IDLE, self.OnIdle) def CreateMenu(self): filemenu = wx.Menu() filemenu.Append(1, "Open") exit = filemenu.Append(2, "Exit") self.Bind(wx.EVT_MENU, self.OnClose, exit) aboutmenu = wx.Menu() aboutmenu.Append(1, "CEF Python") menubar = wx.MenuBar() menubar.Append(filemenu,"&File") menubar.Append(aboutmenu, "&About") self.SetMenuBar(menubar) def OnClose(self, event): # In wx.chromectrl calling browser.CloseBrowser() and/or # self.Destroy() in OnClose is causing crashes when embedding # multiple browser tabs. The solution is to call only # browser.ParentWindowWillClose. Behavior of this example # seems different as it extends wx.Frame, while ChromeWindow # from chromectrl extends wx.Window. Calling CloseBrowser # and Destroy does not cause crashes, but is not recommended. # Call ParentWindowWillClose and event.Skip() instead. See # also Issue 107. self.browser.ParentWindowWillClose() event.Skip() def OnIdle(self, event): cefpython.MessageLoopWork() class MyApp(wx.App): timer = None timerID = 1 timerCount = 0 def OnInit(self): if not USE_EVT_IDLE: self.CreateTimer() frame = MainFrame() self.SetTopWindow(frame) frame.Show() return True def CreateTimer(self): # See "Making a render loop": # http://wiki.wxwidgets.org/Making_a_render_loop # Another approach is to use EVT_IDLE in MainFrame, # see which one fits you better. self.timer = wx.Timer(self, self.timerID) self.timer.Start(10) # 10ms wx.EVT_TIMER(self, self.timerID, self.OnTimer) def OnTimer(self, event): self.timerCount += 1 # print("wxpython.py: OnTimer() %d" % self.timerCount) cefpython.MessageLoopWork() def OnExit(self): # When app.MainLoop() returns, MessageLoopWork() should # not be called anymore. if not USE_EVT_IDLE: self.timer.Stop() if __name__ == '__main__': sys.excepthook = ExceptHook settings = { "debug": False, # cefpython debug messages in console and in log_file "log_severity": cefpython.LOGSEVERITY_INFO, # LOGSEVERITY_VERBOSE "log_file": GetApplicationPath("debug.log"), # Set to "" to disable. "release_dcheck_enabled": True, # Enable only when debugging. # This directories must be set on Linux "locales_dir_path": cefpython.GetModuleDirectory()+"/locales", "resources_dir_path": cefpython.GetModuleDirectory(), "browser_subprocess_path": "%s/%s" % ( cefpython.GetModuleDirectory(), "subprocess") } # print("browser_subprocess_path="+settings["browser_subprocess_path"]) cefpython.Initialize(settings) print('wx.version=%s' % wx.version()) app = MyApp(False) app.MainLoop() # Let wx.App destructor do the cleanup before calling cefpython.Shutdown(). del app cefpython.Shutdown()