当使用 .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.04Raspbian。同样的问题。

#!/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()

您正在为对话框调用 RefreshUpdate,但要更改的不是对话框,而是标签。在某些情况下,对话框的 Update 会导致子部件也被重新绘制,但并非总是如此。然而,在这个例子中有更多的事件需要处理,而不仅仅是新标签的绘制,所以你需要让事件周期性地循环运行,这样其他的事件也可以被处理。当您这样做时,标签的刷新也将得到处理。因此,在您的示例中,将对 RefreshUpdate 的调用替换为对 wx.Yield() 的调用,这样效果会更好。