python圖形化界面設計(wxpython)二分割窗口


本篇內容來自https://www.cnblogs.com/hhudaqiang/p/6566342.html

 

用wxpython開發一個簡單的exe其實很簡單的,但是在開發的過程中會遇到若干的坑、疑問、甚至bug,讓人摸不清頭腦!恰恰關於這方面的文檔是少之又少,看來看去大家還是在官方的文檔上加以引用說明,但是我們在開發的過程中遇到的問題,網上幾乎找不到相關的解答。不知道是大家沒遇到呢?還是遇到解決了不願分享給大家?我本人是個自動化測試工程,在開發領域可以說是菜鳥一枚,只能把自己遇到的問題拿出來和大家分享!也希望大神們踩過的坑,解決的問題能分享出來,讓我們這些小輩們能少踩坑~~好吧,進入今天的主題:wxpython分割窗研究(解決sashPosition=0無效的BUG)!

分割窗在應用的程序開發中是特別常見的,比如robotframework,以及我們python的IDE(PyCharm)的主界面都是分割窗的應用例子,圖片如下:

上面就是3個分割窗,注意的是wxpython最多只支持2個分割窗,如果開發這種分割窗只能用嵌套了!分割子窗口1與2其實是嵌套在畫板1上面的,下面我也介紹如何利用Sizer布局得到這樣的分割窗。

有了上圖直觀的認識后,我也引用個官方的例子,然后從這上面拓展,官方例子如下:

#coding=utf-8
import wx
class Myframe(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self,None)
        self.minpane=0
        self.initpos=0
        self.MakeMenuBar()
        self.sp=wx.SplitterWindow(self)# 創建一個分割窗
        self.p1=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  #創建子面板
        self.p2=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)
        self.p1.SetBackgroundColour("pink")
        self.p2.SetBackgroundColour("blue")
        self.p1.Hide()  # 確保備用的子面板被隱藏
        self.p2.Hide()
        self.sp.Initialize(self.p1)  #  初始化分割窗
        self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGING,self.OnSashChanging,self.sp)
        self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED,self.OnSashChanged,self.sp)
    def OnSplitV(self, evt):  # 響應垂直分割請求
        self.sp.SplitVertically(self.p1, self.p2, self.initpos)
    def MakeMenuBar(self):
        menu=wx.Menu()
        item=menu.Append(-1,"Split horizontally")
        self.Bind(wx.EVT_MENU,self.OnSplitH,item)
        self.Bind(wx.EVT_UPDATE_UI,self.OnCheckCanSplit,item)
        item=menu.Append(-1,"Split vertically")
        self.Bind(wx.EVT_MENU,self.OnSplitV,item)
        self.Bind(wx.EVT_UPDATE_UI,self.OnCheckCanSplit,item)
        item=menu.Append(-1,"Unsplit")
        self.Bind(wx.EVT_MENU,self.OnUnsplit,item)
        self.Bind(wx.EVT_UPDATE_UI,self.OnCheckCanUnsplit,item)
        menu.AppendSeparator()
        item=menu.Append(-1,"Set initial sash position")
        self.Bind(wx.EVT_MENU,self.OnSetPos,item)
        item = menu.Append(-1, "Set minimum pane size")
        self.Bind(wx.EVT_MENU,self.OnSetMin,item)
        menu.AppendSeparator()
        mbar=wx.MenuBar()
        mbar.Append(menu,"Splitter")
        self.SetMenuBar(mbar)
    def OnSashChanging(self,evt):
        print "OnSashChanging:",evt.GetSashPosition()
    def OnSashChanged(self,evt):
        print "OnSashChanged:", evt.GetSashPosition()
    def OnSplitH(self,evt):# 響應水平分割請求
        self.sp.SplitHorizontally(self.p1,self.p2,self.initpos)
    def OnCheckCanSplit(self,evt):
        evt.Enable(not self.sp.IsSplit())
    def OnCheckCanUnsplit(self,evt):
        evt.Enable(self.sp.IsSplit())
    def OnUnsplit(self,evt):
        self.sp.Unsplit()
    def OnSetMin(self,evt):
        minpane=wx.GetNumberFromUser("Enter the minimum pane size","","Minimum Pane Size",self.minpane,0,1000,self)
        if minpane!=-1:
            self.minpane=minpane
            self.sp.SetMinimumPaneSize(self.minpane)
    def OnSetPos(self,evt):
        initpos=wx.GetNumberFromUser("Enter the initial sash position (to be used in the Split call)","","Initial Sash Position",self.initpos,-1000,1000,self)
        if initpos!=-1:
            pass
app = wx.PySimpleApp()
frame=Myframe()
frame.Show(True)
app.MainLoop()

程序是很簡單的,但是涵蓋了分割的基本方法,運行一下是沒有問題的。菜單包含着以垂直或水平方式來分割窗口,針對上面的例子我想說明幾點!

1.分割窗只能分割一次,對已分割的窗口再分割將會失敗,要確定窗口當前是否被分割了,調用方法 IsSplit()。

2.如果你想不分割窗口或者重新分割窗口那么必須使用Unsplit(toRemove=None)。參數toRemove 是實際要移除的wx.Window對象,並且必須是這兩個子窗口中的一個。如果 toRemove是None,那么底部或右部的窗口將被移除。

3.如果只有一個窗口分割調用Initialize(window)。如果2個窗口分割者調用SplitHorizontally (window1,window2,sashPosition=0)或 SplitVertically(window1, window2, sashPosition=0)。參數window1和window2是兩個子窗口,參數sashPosition包含分割條的初始位置。對於水平分割(垂直分割)來說,window1被放置在window2的頂部(左邊)。如果sashPosition是一個正數,它代表分割條距頂部的初始高度(分割條距左邊框的初始寬度)。如果sashPosition是一個負數,它定義了分割條距底部的高度值(分割條距右邊框的初始寬度)。如果sashPosition是0,那么這個分割條位於正中

 

那么我們的問題來了,如果我啟動這個應用程序就想得到一個已經水平分割好的且分割條居中窗口,怎么辦?畢竟我們實際的應用程序不太可能讓你通過菜單來切換吧,而且布局都是固定的。可能有的人覺得我提出這個問題有點小白,看我啪啪的給你敲出幾行代碼來。最簡單的代碼如下:

#coding=utf-8
import wx
class Myframe(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self,None)
        self.sp=wx.SplitterWindow(self)# 創建一個分割窗
        self.p1=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  #創建子面板
        self.p2=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)
        self.p1.SetBackgroundColour("pink")
        self.p2.SetBackgroundColour("blue")
        self.p1.Hide()  # 確保備用的子面板被隱藏
        self.p2.Hide()
        self.sp.SplitHorizontally(self.p1,self.p2,0)  # 代表分割條居中
app = wx.PySimpleApp()
frame=Myframe()
frame.Show(True)
app.MainLoop()

先申明一下我的wx版本是2.8的,運行平台是64 bit的win8系統,運行結果讓我大跌眼鏡:

這是什么東西,我設置的sashPosition=0啊,說好的分割條居中呢,這也太不靠譜了吧,我反正是實在找不出這段代碼寫的有啥問題(知道哪里錯的大哥一定要告訴我^ ^),我姑且認為這是版本的一個bug吧,我相信好多同學和我遇到一樣的問題。當然如果我們給sashPosition設置參數比如sashPosition=100,讓分割條距離頂部100個像素表現是正常的。我又不相信愛情了,明明我們在上面官方的例子里利用菜單分割是正常的,為什么我們在Frame的構造函數里面分割就不行了呢,想了想唯一的不同可能是菜單在水平分割的時候要調用Unsplit()方法取消分割,但是我們在構造函數里面也沒有分割啊,不管怎么樣我們在分割之前也調用這個看看效果如何,不過遺憾的是然並卵。在這個時候我突然想到我在給Panel設置背景圖片時用到了wx.EVT_ERASE_BACKGROUND事件,我們本着曲線救國的思想來試下吧,修改后的程序如下:

#coding=utf-8
import wx
class Myframe(wx.Frame):
    def __init__(self,flag=False):
        wx.Frame.__init__(self,None)
        self.sp=wx.SplitterWindow(self)# 創建一個分割窗
        self.flag=flag
        self.fisrt=0
        self.p1=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  #創建子面板
        self.p2=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)
        self.p1.SetBackgroundColour("pink")
        self.p2.SetBackgroundColour("blue")
        self.p1.Hide()  # 確保備用的子面板被隱藏
        self.p2.Hide()
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBack)
        self.sp.Unsplit()
        self.sp.SplitHorizontally(self.p1, self.p2,0)
    def OnEraseBack(self,event):
        if self.fisrt<2 or self.flag :
            self.sp.SetSashPosition(0)
            self.fisrt=self.fisrt+1
app = wx.PySimpleApp()
frame=Myframe(True)
frame.Show(True)
app.MainLoop()

我們懷着試一試的心態去試下,結果如下:

說明sashPosition=0奏效了。還有wx.EVT_ERASE_BACKGROUND當背景需要重新繪制時就會觸發,應用程序啟用的時候會調用2次。重要的是我們使用了self.sp.SetSashPosition()這個函數,它的作用是重新定義分割條的位置,這里我們解釋下self.first和self.flag。我么考慮到動態分割的問題,比如我啟動這個應用程序的時候它是均等分割的,但是我們改變這個框架大小的時候,我們有2種選擇,一種就是不再動態分割,一種繼續動態平均分割。

1.self.first<2是保證我們初始啟動這個應用程序的時候能調用self.sp.SetSashPosition()這個函數,這時候分割條是在中間的。

2.self.flag如果我們設置為True那么當我們改變frame框架大小的時候self.sp.SetSashPosition()一直被調用,達到動態均分的目的,如果false則self.SetSashPosition()不再被調用,具體可以自己設置感受。

下面我們討論3種我認為可能用到的場景。

第一種場景:我們實現一個如下布局的分割窗:

這個嵌套的分割窗特點是:水平分割窗均分,2個子垂直分割窗也均分。代碼如下:

#coding=utf-8
import wx
import time
class Myframe(wx.Frame):
    def __init__(self,flag=True):
        wx.Frame.__init__(self,None)
        self.first=0
        self.flag=flag
        self.sp=wx.SplitterWindow(self)# 創建一個分割窗,parent是frame
        self.p1=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  #創建子面板p1
        self.p2=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  # 創建子面板p2
        self.p1.Hide()  # 確保備用的子面板被隱藏
        self.p2.Hide()
        self.sp1 = wx.SplitterWindow(self.p1)  # 創建一個子分割窗,parent是p1
        self.box = wx.BoxSizer(wx.VERTICAL)#創建一個垂直布局
        self.box.Add(self.sp1, 1, wx.EXPAND)#將子分割窗布局延伸至整個p1空間
        self.p1.SetSizer(self.box)
        self.p2.SetBackgroundColour("blue")
        self.p1_1 = wx.Panel(self.sp1, style=wx.SUNKEN_BORDER)#在子分割窗self.sp1的基礎上創建子畫板p1_1
        self.p1_2 = wx.Panel(self.sp1, style=wx.SUNKEN_BORDER)#在子分割窗self.sp1的基礎上創建子畫板p1_2
        self.p1_1.Hide()
        self.p1_2.Hide()
        self.p1_1.SetBackgroundColour("red")
        self.p1_2.SetBackgroundColour("yellow")
        self.sp.SplitHorizontally(self.p1, self.p2, 0)
        self.sp1.SplitVertically(self.p1_1, self.p1_2, 0)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBack)
    def OnEraseBack(self,event):
        if self.first<2 or self.flag:
            self.sp.SetSashPosition(0)
            self.sp1.SetSashPosition(0)
            self.first=self.first+1
        self.Refresh()
app = wx.PySimpleApp()
frame=Myframe(True)
frame.Show(True)
app.MainLoop()

代碼很簡單,下面我說下我這個程序的想法。首先,來一次水平分割窗,這是沒有疑問的。然后在上面的分割窗的畫板中嵌套一個垂直分割窗,那么我們就要鋪滿上面的這個畫板。我們或許可以計算self.p1的寬度喝高度然后self.sp1 = wx.SplitterWindow(self.p1,size=(width,height)),但是,你這個要是動態的,比如frame窗口被放大或者縮小了,要監聽wx.EVT_SIZE事件,動態改變self.p1的寬度和高度,但是如果這樣就太舍近求遠了,所以我們想到BoxSizer布局,利用proportion=1和wx.EXPAND讓它動態的總是填滿self.p1。其次,self.flag和self.first參數意義上面已經說明。如果想每次Frame窗口大小變動時,都會動態均分布局,那么就使self.flap=True吧。好了這個布局你會了是吧。

第二種場景:與上面的類似我們實現一個如下布局的分割窗:

這種嵌套分割窗的特點是,水平分割線距離底部固定的距離,垂直分割線距離左邊框也固定的距離。其實我們的python IDE(pycharm)就是這種格局,代碼也很簡單,貼出來一下吧,就在上面的例子中,改變sashPosition為固定的值,其中值得正負代表不同的含義,上面我已經說了,代碼如下:

#coding=utf-8
import wx
import time
class Myframe(wx.Frame):
    def __init__(self,flag=True):
        wx.Frame.__init__(self,None)
        self.first=0
        self.flag=flag
        self.sp=wx.SplitterWindow(self)# 創建一個分割窗,parent是frame
        self.p1=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  #創建子面板p1
        self.p2=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  # 創建子面板p2
        self.p1.Hide()  # 確保備用的子面板被隱藏
        self.p2.Hide()
        self.sp1 = wx.SplitterWindow(self.p1)  # 創建一個子分割窗,parent是p1
        self.box = wx.BoxSizer(wx.VERTICAL)#創建一個垂直布局
        self.box.Add(self.sp1, 1, wx.EXPAND)#將子分割窗布局延伸至整個p1空間
        self.p1.SetSizer(self.box)
        self.p2.SetBackgroundColour("blue")
        self.p1_1 = wx.Panel(self.sp1, style=wx.SUNKEN_BORDER)#在子分割窗self.sp1的基礎上創建子畫板p1_1
        self.p1_2 = wx.Panel(self.sp1, style=wx.SUNKEN_BORDER)#在子分割窗self.sp1的基礎上創建子畫板p1_2
        self.p1_1.Hide()
        self.p1_2.Hide()
        self.p1_1.SetBackgroundColour("red")
        self.p1_2.SetBackgroundColour("yellow")
        self.sp.SplitHorizontally(self.p1, self.p2, 0)
        self.sp1.SplitVertically(self.p1_1, self.p1_2, 0)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBack)
    def OnEraseBack(self,event):
        if self.first<2 or self.flag:
            self.sp.SetSashPosition(100)
            self.sp1.SetSashPosition(-100)
            self.first=self.first+1
     self.Refresh()
app = wx.PySimpleApp()
frame=Myframe(True)
frame.Show(True)
app.MainLoop()

這個沒什么解釋了吧,具體參數意義與上面一直,其實Pycharm的用的是self.flag=True的風格。

第三種:我想實現了水平分割和垂直分割動態分配,比如水平分割底部窗口占用frame框架高度的1/4(頂部3/4),垂直分割模式左邊窗口占用frame框架寬度的1/4(右邊3/4),那么我們可能要監聽EVT_SIZE事件了,具體代碼如下:

#coding=utf-8
import wx
class Myframe(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self,None)
        self.width=self.Size.width
        self.height=self.Size.height
        self.up = self.height/3*2 #上面窗口高度
        self.left = self.width/ 4#嵌套窗口左窗口寬度
        self.sp=wx.SplitterWindow(self,size=(self.width,self.height))# 創建一個分割窗
        self.p1=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)  #創建子面板
        self.p2=wx.Panel(self.sp,style=wx.SUNKEN_BORDER)
        self.box = wx.BoxSizer(wx.VERTICAL)
        self.sp1 = wx.SplitterWindow(self.p1)  # 創建一個子分割窗
        self.box.Add(self.sp1,1,wx.EXPAND)
        self.p1.SetSizer(self.box)
        self.p2.SetBackgroundColour("blue")
        self.p1_1=wx.Panel(self.sp1,style=wx.SUNKEN_BORDER)
        self.p1_2=wx.Panel(self.sp1,style=wx.SUNKEN_BORDER)
        self.p1_1.Hide()
        self.p1_2.Hide()
        self.p1_1.SetBackgroundColour("red")
        self.p1_2.SetBackgroundColour("yellow")
        self.p1.Hide()  # 確保備用的子面板被隱藏
        self.p2.Hide()
        self.sp1.SplitVertically(self.p1_1, self.p1_2, self.left)
        self.sp.SplitHorizontally(self.p1, self.p2, self.up)
        self.Bind(wx.EVT_SIZE,self.SizeOnchange)
    def SizeOnchange(self,evt):
        size = evt.Size
        self.sp.Size=size#這一句很重要
        self.sp.SetSashPosition(size.height/3*2)
        self.sp1.SetSashPosition(size.width/4 )

app = wx.PySimpleApp()
frame=Myframe()
frame.Show(True)
app.MainLoop()

代碼與上面大同小異,主要的區別就是監聽EVT_SIZE事件,動態的改變self.sp的大小,以及水平分割條和垂直分割條的相對寬度和高度。圖片就不貼了吧,其實這個也可以取代上面的代碼。主要通過一些手段達到我們的目的!!關於分割窗我想應該講的很清楚了吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM