当使用 .Wrap 时,wxpython StaticText.SetLabel 不会在单线程 GUI 中更新?
wxpython StaticText.SetLabel doesn't update in a single-threaded GUI when .Wrap is used?
我有一个很长的 运行ning python 脚本,目前是终端驱动的。我需要在流程上放置一个 GUI 前端,以使其更加用户友好。
目前 GUI 和 long-运行ning-process 都 运行 在同一个线程中。 (我计划在未来通过为 long 函数生成一个新线程来改进这一点)。我已经为那个长 运行ning 线程提供了一些回调,以便它可以偶尔更新 GUI 以帮助衡量其进度。
出于某种原因,回调能够直观地更新位图,但不能更新标签,即使在这两种情况下都调用了 .Refresh() 和 .Update()。
我注意到,如果我在 on_user_action_label_wrap
中删除对 wrap
的调用,那么它 会 更新标签和位图。
问题
a) 为什么使用.Wrap
会影响标签的更新?
b) **是否可以在`on_user_action_label_wrap中强制使用标签的update/repaint?或者真的,只是将文本换行并更新? **
('Use multiple threads' 不是一个可接受的答案:)这最终会发生,但现在我想了解为什么这不起作用)
下面是一个SSCCE。我在 python 2.7、wxpython 3.0.2.0、wx-3.0-gtk2、gtk+ 2.24.30-1ubuntu1 和 2.24.25- 上 运行 这个(和我的原始代码) 3+deb8u1,以及 Ubuntu 16.04 和 Raspbian。同样的问题。
#!/usr/bin/env python
# coding=utf-8
from __future__ import absolute_import, division, print_function
import collections
import wx
from time import sleep
STATE_START = "Start!"
STATE_MIDDLE = "Middle!"
STATE_END = "End!"
STATE_STOP = "Stop!"
STATES = [STATE_START, STATE_MIDDLE, STATE_END, STATE_STOP]
class SingleThreadedKeyLoader(wx.Dialog):
ICON_SIZE = (32, 32)
def __init__(self, parent):
wx.Dialog.__init__(self, parent, title="Do stuff",
size=wx.Size(800, 600),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.STAY_ON_TOP)
self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize)
self.active_state_bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD,
wx.ART_CMN_DIALOG,
self.ICON_SIZE)
self.done_state_bmp = wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK,
wx.ART_CMN_DIALOG,
self.ICON_SIZE)
if wx.NullBitmap in [self.active_state_bmp, self.done_state_bmp]:
raise Exception("Failed to load icons")
self._make_ui()
self.current_state = STATE_START
def _make_ui(self):
dialog_sizer = wx.BoxSizer(wx.VERTICAL)
top_half_sizer = wx.BoxSizer(wx.HORIZONTAL)
#########
# User action label
label_sizer = wx.BoxSizer(wx.VERTICAL)
self.panel = wx.Panel(self, wx.ID_ANY)
self.user_action_label = wx.StaticText(self.panel,
label="",
style=wx.ALIGN_CENTRE)
self.user_action_label.Wrap(100) # DOESN't SEEM TO WORK <---------
self.user_action_label.Bind(wx.EVT_SIZE,
self.on_user_action_label_wrap)
label_sizer.Add(self.user_action_label, 1,
wx.EXPAND | wx.ALIGN_CENTER | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL,
5)
self.panel.SetSizer(label_sizer)
top_half_sizer.Add(self.panel, 1, wx.EXPAND, 5)
#
#########
#########
# combo box + log area. Just here to get in the way of long text
log_area_sizer = wx.BoxSizer(wx.VERTICAL)
self.log_textbox = wx.TextCtrl(self, value=wx.EmptyString,
style=wx.TE_MULTILINE | wx.TE_READONLY)
log_area_sizer.Add(self.log_textbox, 1,
wx.ALIGN_TOP | wx.ALL | wx.EXPAND, 5)
self.log_combobox_choices = ["Debug", "Info", "Warning"]
self.log_combobox = wx.ComboBox(self, value="Info",
choices=self.log_combobox_choices)
log_area_sizer.Add(self.log_combobox, 0,
wx.ALIGN_BOTTOM | wx.ALL | wx.EXPAND, 5)
top_half_sizer.Add(log_area_sizer, 2, wx.EXPAND, 5)
dialog_sizer.Add(top_half_sizer, 2, wx.EXPAND, 5)
#
#########
#########
# State tracking bitmaps. The image should be updated as long function
# runs.
state_tracker_sizer = wx.WrapSizer(wx.HORIZONTAL)
self.bitmaps_od = self.generate_state_tracker(self,
state_tracker_sizer)
dialog_sizer.Add(state_tracker_sizer, 1, wx.EXPAND, 5)
#
#########
#########
# Buttons! A, B, C are here just to check the text-wrap is working
# in the box.
button_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.button_a = wx.Button(self, label=u"A")
self.button_a.Bind(wx.EVT_BUTTON, self.on_clicked_a)
button_sizer.Add(self.button_a, 0, wx.ALL, 5)
self.button_b = wx.Button(self, label=u"B")
self.button_b.Bind(wx.EVT_BUTTON, self.on_clicked_b)
button_sizer.Add(self.button_b, 0, wx.ALL, 5)
self.button_c = wx.Button(self, label=u"C")
self.button_c.Bind(wx.EVT_BUTTON, self.on_clicked_c)
button_sizer.Add(self.button_c, 0, wx.ALL, 5)
self.button_long_func = wx.Button(self, label=u"Long running func")
self.button_long_func.Bind(wx.EVT_BUTTON, self.on_clicked_long_func)
button_sizer.Add(self.button_long_func, 0, wx.ALL, 5)
dialog_sizer.Add(button_sizer, 0, wx.EXPAND, 5)
#
#########
self.SetSizer(dialog_sizer)
self.Layout()
self.Centre(wx.BOTH)
@staticmethod
def generate_state_tracker(parent, state_tracker_sizer):
"""
Generates the staticbitmap and statictext.
The bitmaps will change from arrows to ticks during the process
"""
def state_label(i, state):
return "{:>02}. {}".format(i + 1, str(state))
f = parent.GetFont()
dc = wx.WindowDC(parent)
dc.SetFont(f)
label_bitmaps = collections.OrderedDict()
max_string_width = -1
for i, state in enumerate(STATES):
width, height = dc.GetTextExtent(state_label(i, state))
max_string_width = max(max_string_width, width)
for i, state in enumerate(STATES):
state_sizer = wx.BoxSizer(wx.HORIZONTAL)
bitmap_name = "state{}_{}".format(i, "bitmap")
bitmap = wx.StaticBitmap(parent, bitmap=wx.NullBitmap,
name=bitmap_name,
size=SingleThreadedKeyLoader.ICON_SIZE)
state_sizer.Add(bitmap, 0, wx.ALL, 5)
label_bitmaps[bitmap_name] = bitmap
label_bitmaps[state] = bitmap
label_sizer = wx.BoxSizer(wx.HORIZONTAL)
label = wx.StaticText(parent, label=state_label(i, state),
size=wx.Size(max_string_width, -1))
label_sizer.Add(label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 0)
state_sizer.Add(label_sizer, 1, wx.EXPAND, 5)
state_tracker_sizer.Add(state_sizer, 1, wx.EXPAND, 5)
return label_bitmaps
def on_clicked_a(self, event):
""" wrapping test: Sets the label to a small amount of text"""
self.user_action_label.SetLabel("A this is a small label")
def on_clicked_b(self, event):
""" wrapping test: Sets the label to a medium amount of text"""
self.user_action_label.SetLabel(
"B B B This is a medium label. It is longer than a small label but shorter than the longer label")
def on_clicked_c(self, event):
""" wrapping test: Sets the label to a large amount of text"""
self.user_action_label.SetLabel(
"C C C C C C C This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. ")
def my_set_state_callback(self, new_state):
"""
Updates the lovely tickboxes.
Used as a callback from the long running function.
"""
if new_state == self.current_state:
return
print(self.current_state, "->", new_state)
self.bitmaps_od[self.current_state].SetBitmap(self.done_state_bmp)
self.bitmaps_od[new_state].SetBitmap(self.active_state_bmp)
self.current_state = new_state
self.Refresh()
self.Update()
def my_user_feedback_callback(self, message):
"""
Updates the label that insturcts the user to do stuff
Used as a callback from the long running function.
"""
print("my_user_feedback_fn", message)
self.user_action_label.SetLabel(message)
self.Refresh()
self.Update()
def on_clicked_long_func(self, event):
""" launches the long function on button press """
self.button_a.Enable(False)
self.button_b.Enable(False)
self.button_c.Enable(False)
self.log_combobox.Enable(False)
self.button_long_func.Enable(False)
self.bitmaps_od[STATE_START].SetBitmap(self.active_state_bmp)
wx.CallAfter(my_long_running_func, self.my_set_state_callback,
self.my_user_feedback_callback)
def on_user_action_label_wrap(self, event):
""" Wraps the long text in the box on resize. """
self.user_action_label.Wrap(self.panel.Size[0]) #<------------------------ problem?
event.Skip() # ?
def my_long_running_func(set_state, user_feedback_fn):
"""
This is a very long runing function that uses callbacks to update the
main UI.
"""
user_feedback_fn(
"This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. ")
set_state(STATE_START)
sleep(2)
set_state(STATE_MIDDLE)
sleep(2)
user_feedback_fn("do the thing")
# If this was the real thing then here we'd pause until the user obeyed
# the instruction or something
# wait_for_user_to_do_the_thing()
set_state(STATE_END)
sleep(0.5)
user_feedback_fn("DONE")
set_state(STATE_STOP)
print("returning")
return
app = wx.App(False)
x = SingleThreadedKeyLoader(None)
x.ShowModal()
x.Destroy()
app.MainLoop()
您正在为对话框调用 Refresh
和 Update
,但要更改的不是对话框,而是标签。在某些情况下,对话框的 Update
会导致子部件也被重新绘制,但并非总是如此。然而,在这个例子中有更多的事件需要处理,而不仅仅是新标签的绘制,所以你需要让事件周期性地循环运行,这样其他的事件也可以被处理。当您这样做时,标签的刷新也将得到处理。因此,在您的示例中,将对 Refresh
和 Update
的调用替换为对 wx.Yield()
的调用,这样效果会更好。
我有一个很长的 运行ning python 脚本,目前是终端驱动的。我需要在流程上放置一个 GUI 前端,以使其更加用户友好。
目前 GUI 和 long-运行ning-process 都 运行 在同一个线程中。 (我计划在未来通过为 long 函数生成一个新线程来改进这一点)。我已经为那个长 运行ning 线程提供了一些回调,以便它可以偶尔更新 GUI 以帮助衡量其进度。
出于某种原因,回调能够直观地更新位图,但不能更新标签,即使在这两种情况下都调用了 .Refresh() 和 .Update()。
我注意到,如果我在 on_user_action_label_wrap
中删除对 wrap
的调用,那么它 会 更新标签和位图。
问题
a) 为什么使用.Wrap
会影响标签的更新?
b) **是否可以在`on_user_action_label_wrap中强制使用标签的update/repaint?或者真的,只是将文本换行并更新? **
('Use multiple threads' 不是一个可接受的答案:)这最终会发生,但现在我想了解为什么这不起作用)
下面是一个SSCCE。我在 python 2.7、wxpython 3.0.2.0、wx-3.0-gtk2、gtk+ 2.24.30-1ubuntu1 和 2.24.25- 上 运行 这个(和我的原始代码) 3+deb8u1,以及 Ubuntu 16.04 和 Raspbian。同样的问题。
#!/usr/bin/env python
# coding=utf-8
from __future__ import absolute_import, division, print_function
import collections
import wx
from time import sleep
STATE_START = "Start!"
STATE_MIDDLE = "Middle!"
STATE_END = "End!"
STATE_STOP = "Stop!"
STATES = [STATE_START, STATE_MIDDLE, STATE_END, STATE_STOP]
class SingleThreadedKeyLoader(wx.Dialog):
ICON_SIZE = (32, 32)
def __init__(self, parent):
wx.Dialog.__init__(self, parent, title="Do stuff",
size=wx.Size(800, 600),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.STAY_ON_TOP)
self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize)
self.active_state_bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD,
wx.ART_CMN_DIALOG,
self.ICON_SIZE)
self.done_state_bmp = wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK,
wx.ART_CMN_DIALOG,
self.ICON_SIZE)
if wx.NullBitmap in [self.active_state_bmp, self.done_state_bmp]:
raise Exception("Failed to load icons")
self._make_ui()
self.current_state = STATE_START
def _make_ui(self):
dialog_sizer = wx.BoxSizer(wx.VERTICAL)
top_half_sizer = wx.BoxSizer(wx.HORIZONTAL)
#########
# User action label
label_sizer = wx.BoxSizer(wx.VERTICAL)
self.panel = wx.Panel(self, wx.ID_ANY)
self.user_action_label = wx.StaticText(self.panel,
label="",
style=wx.ALIGN_CENTRE)
self.user_action_label.Wrap(100) # DOESN't SEEM TO WORK <---------
self.user_action_label.Bind(wx.EVT_SIZE,
self.on_user_action_label_wrap)
label_sizer.Add(self.user_action_label, 1,
wx.EXPAND | wx.ALIGN_CENTER | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL,
5)
self.panel.SetSizer(label_sizer)
top_half_sizer.Add(self.panel, 1, wx.EXPAND, 5)
#
#########
#########
# combo box + log area. Just here to get in the way of long text
log_area_sizer = wx.BoxSizer(wx.VERTICAL)
self.log_textbox = wx.TextCtrl(self, value=wx.EmptyString,
style=wx.TE_MULTILINE | wx.TE_READONLY)
log_area_sizer.Add(self.log_textbox, 1,
wx.ALIGN_TOP | wx.ALL | wx.EXPAND, 5)
self.log_combobox_choices = ["Debug", "Info", "Warning"]
self.log_combobox = wx.ComboBox(self, value="Info",
choices=self.log_combobox_choices)
log_area_sizer.Add(self.log_combobox, 0,
wx.ALIGN_BOTTOM | wx.ALL | wx.EXPAND, 5)
top_half_sizer.Add(log_area_sizer, 2, wx.EXPAND, 5)
dialog_sizer.Add(top_half_sizer, 2, wx.EXPAND, 5)
#
#########
#########
# State tracking bitmaps. The image should be updated as long function
# runs.
state_tracker_sizer = wx.WrapSizer(wx.HORIZONTAL)
self.bitmaps_od = self.generate_state_tracker(self,
state_tracker_sizer)
dialog_sizer.Add(state_tracker_sizer, 1, wx.EXPAND, 5)
#
#########
#########
# Buttons! A, B, C are here just to check the text-wrap is working
# in the box.
button_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.button_a = wx.Button(self, label=u"A")
self.button_a.Bind(wx.EVT_BUTTON, self.on_clicked_a)
button_sizer.Add(self.button_a, 0, wx.ALL, 5)
self.button_b = wx.Button(self, label=u"B")
self.button_b.Bind(wx.EVT_BUTTON, self.on_clicked_b)
button_sizer.Add(self.button_b, 0, wx.ALL, 5)
self.button_c = wx.Button(self, label=u"C")
self.button_c.Bind(wx.EVT_BUTTON, self.on_clicked_c)
button_sizer.Add(self.button_c, 0, wx.ALL, 5)
self.button_long_func = wx.Button(self, label=u"Long running func")
self.button_long_func.Bind(wx.EVT_BUTTON, self.on_clicked_long_func)
button_sizer.Add(self.button_long_func, 0, wx.ALL, 5)
dialog_sizer.Add(button_sizer, 0, wx.EXPAND, 5)
#
#########
self.SetSizer(dialog_sizer)
self.Layout()
self.Centre(wx.BOTH)
@staticmethod
def generate_state_tracker(parent, state_tracker_sizer):
"""
Generates the staticbitmap and statictext.
The bitmaps will change from arrows to ticks during the process
"""
def state_label(i, state):
return "{:>02}. {}".format(i + 1, str(state))
f = parent.GetFont()
dc = wx.WindowDC(parent)
dc.SetFont(f)
label_bitmaps = collections.OrderedDict()
max_string_width = -1
for i, state in enumerate(STATES):
width, height = dc.GetTextExtent(state_label(i, state))
max_string_width = max(max_string_width, width)
for i, state in enumerate(STATES):
state_sizer = wx.BoxSizer(wx.HORIZONTAL)
bitmap_name = "state{}_{}".format(i, "bitmap")
bitmap = wx.StaticBitmap(parent, bitmap=wx.NullBitmap,
name=bitmap_name,
size=SingleThreadedKeyLoader.ICON_SIZE)
state_sizer.Add(bitmap, 0, wx.ALL, 5)
label_bitmaps[bitmap_name] = bitmap
label_bitmaps[state] = bitmap
label_sizer = wx.BoxSizer(wx.HORIZONTAL)
label = wx.StaticText(parent, label=state_label(i, state),
size=wx.Size(max_string_width, -1))
label_sizer.Add(label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 0)
state_sizer.Add(label_sizer, 1, wx.EXPAND, 5)
state_tracker_sizer.Add(state_sizer, 1, wx.EXPAND, 5)
return label_bitmaps
def on_clicked_a(self, event):
""" wrapping test: Sets the label to a small amount of text"""
self.user_action_label.SetLabel("A this is a small label")
def on_clicked_b(self, event):
""" wrapping test: Sets the label to a medium amount of text"""
self.user_action_label.SetLabel(
"B B B This is a medium label. It is longer than a small label but shorter than the longer label")
def on_clicked_c(self, event):
""" wrapping test: Sets the label to a large amount of text"""
self.user_action_label.SetLabel(
"C C C C C C C This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. ")
def my_set_state_callback(self, new_state):
"""
Updates the lovely tickboxes.
Used as a callback from the long running function.
"""
if new_state == self.current_state:
return
print(self.current_state, "->", new_state)
self.bitmaps_od[self.current_state].SetBitmap(self.done_state_bmp)
self.bitmaps_od[new_state].SetBitmap(self.active_state_bmp)
self.current_state = new_state
self.Refresh()
self.Update()
def my_user_feedback_callback(self, message):
"""
Updates the label that insturcts the user to do stuff
Used as a callback from the long running function.
"""
print("my_user_feedback_fn", message)
self.user_action_label.SetLabel(message)
self.Refresh()
self.Update()
def on_clicked_long_func(self, event):
""" launches the long function on button press """
self.button_a.Enable(False)
self.button_b.Enable(False)
self.button_c.Enable(False)
self.log_combobox.Enable(False)
self.button_long_func.Enable(False)
self.bitmaps_od[STATE_START].SetBitmap(self.active_state_bmp)
wx.CallAfter(my_long_running_func, self.my_set_state_callback,
self.my_user_feedback_callback)
def on_user_action_label_wrap(self, event):
""" Wraps the long text in the box on resize. """
self.user_action_label.Wrap(self.panel.Size[0]) #<------------------------ problem?
event.Skip() # ?
def my_long_running_func(set_state, user_feedback_fn):
"""
This is a very long runing function that uses callbacks to update the
main UI.
"""
user_feedback_fn(
"This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. ")
set_state(STATE_START)
sleep(2)
set_state(STATE_MIDDLE)
sleep(2)
user_feedback_fn("do the thing")
# If this was the real thing then here we'd pause until the user obeyed
# the instruction or something
# wait_for_user_to_do_the_thing()
set_state(STATE_END)
sleep(0.5)
user_feedback_fn("DONE")
set_state(STATE_STOP)
print("returning")
return
app = wx.App(False)
x = SingleThreadedKeyLoader(None)
x.ShowModal()
x.Destroy()
app.MainLoop()
您正在为对话框调用 Refresh
和 Update
,但要更改的不是对话框,而是标签。在某些情况下,对话框的 Update
会导致子部件也被重新绘制,但并非总是如此。然而,在这个例子中有更多的事件需要处理,而不仅仅是新标签的绘制,所以你需要让事件周期性地循环运行,这样其他的事件也可以被处理。当您这样做时,标签的刷新也将得到处理。因此,在您的示例中,将对 Refresh
和 Update
的调用替换为对 wx.Yield()
的调用,这样效果会更好。