wxpython分割窗研究(解决sashPosition=0无效的BUG)
用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,那么这个分割条位于正中。
那么我们的问题来了,如果我启动这个应用程序就想得到一个已经水平分割好的且分割条居中窗口,怎么办?毕竟我们实际的应用程序不太可能让你通过菜单来切换吧,而且布局都是固定的。可能有的人觉得我提出这个问题有点小白,看我啪啪的给你敲出几行代码来。最简单的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#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的大小,以及水平分割条和垂直分割条的相对宽度和高度。图片就不贴了吧,其实这个也可以取代上面的代码。主要通过一些手段达到我们的目的!!关于分割窗我想应该讲的很清楚了吧。