Writing texts from two threads in native_windows_gui::TextBox not possible

Hello,
currently I am working on a GUI based on native_windows_gui and native_windows_derive. The GUI has two buttons and a textfield. If the start button is clicked, two threads(start_threads) should write "Lorem" in the textfield. At the moment the gui prints the text in the console. Up to now I am not able to write a text from the two threads in this textfield.
Executing

self.text_box.set_text("Lorem");

in the thread is not possible
I have also tried to protect the textfield with Arc(Mutex), but then I get the errors "*mut winapi::shared::windef::HWND__ cannot be shared between threads safely" and "*mut winapi::shared::windef::HMENU__ cannot be shared between threads safely":

let text_box=Arc::new(Mutex::new(&self.text_box));

at the beginning of the method start_thread and

text_box.lock().unwrap().set_text("LOREM");"

one line above the sleep-command.
Any hints what I am doing wrong? Or is it not possible? I have to admit, that I am quit new in Rust.

Cargo.toml
[package]
name = "gui"
version = "0.1.0"
edition = "2024"

[dependencies]
native-windows-gui = "1.0.12"
native-windows-derive = "1.0.3"
rand = "0.8"
chrono = "0.4"

main.rs
use native_windows_gui as nwg;
use native_windows_derive::NwgUi;
use std::{sync::{Arc, Mutex, mpsc}, thread, time::Duration};
use std::sync::MutexGuard;
use rand::{thread_rng, Rng};
use crate::nwg::NativeUi;
use chrono::Local;
use native_windows_gui::TextBox;

#[derive(NwgUi)]
pub struct GitBackup {
    sender:  mpsc::Sender<String>,
    receiver: Arc<Mutex<mpsc::Receiver<String>>>,

    #[nwg_control(size: (300, 200), title: "GIT Backup", flags: "WINDOW|VISIBLE|MINIMIZE_BOX")]
    #[nwg_events( OnWindowClose: [GitBackup::exit], OnWindowMinimize: [GitBackup::minimize] )]
    window: nwg::Window,
 
    #[nwg_control(parent: window, text: "Starten", position: (20, 20), size: (120, 40))]
    #[nwg_events( OnButtonClick: [GitBackup::start_threads] )]
    start_button: nwg::Button,
 
    #[nwg_control(parent: window, text: "Stoppen", position: (160, 20), size: (120, 40))]
    #[nwg_events( OnButtonClick: [GitBackup::stop_threads] )]
    stop_button: nwg::Button,

    #[nwg_control(parent: window, position: (20, 80), size: (260, 40), text: "", flags: "VISIBLE|VSCROLL|AUTOVSCROLL", readonly: true)]
    text_box: nwg::TextBox,

    #[nwg_control(parent: window, icon: Some(&data.icon), tip: Some("Rust GUI"))]
    #[nwg_events( MousePressLeftUp: [GitBackup::restore] )]
    tray: nwg::TrayNotification,

    #[nwg_resource(source_file: Some("icon.ico"))]
    icon: nwg::Icon,

    #[nwg_resource]
    font: nwg::Font,
    
    threads: Arc<Mutex<Vec<thread::JoinHandle<()>>>>,
    stop_signal: Arc<Mutex<bool>>,

}

 

impl Default for GitBackup
{

    //TODO!!

    fn default() -> Self
    {

        let (sender, receiver) = mpsc::channel();
        Self{
            text_box: nwg::TextBox::default(),
            tray:Default::default(),
            icon:Default::default(),
            stop_button:Default::default(),
            start_button:Default::default(),
            window:Default::default(),
            font:Default::default(),
            threads:Default::default(),
            sender,
            receiver:Arc::new(Mutex::new(receiver)),
            stop_signal:Default::default()
        }
    }

}

impl GitBackup {

    fn start_threads(&self) {
        let stop_signal = Arc::clone(&self.stop_signal);
        *stop_signal.lock().unwrap() = false;
        for _ in 0..2
        {
            let stop_signal = Arc::clone(&stop_signal);
            let handle = thread::spawn( move || {
                //If stop_signal==true -> die!!
                while !*stop_signal.lock().unwrap() {
                    println!("Lorem");
                    thread::sleep(Duration::from_millis(1500));
                }
            });
        }
    }

    fn stop_threads(&self) {
        *self.stop_signal.lock().unwrap() = true;
        let mut threads = self.threads.lock().unwrap();
        threads.clear();
    }

    fn minimize(&self) {
        self.window.set_visible(false);
        let flags = nwg::TrayNotificationFlags::USER_ICON | nwg::TrayNotificationFlags::LARGE_ICON;
        self.tray.show("",Some("Blabla"),Some(flags), Some(&self.icon));
    }

    fn restore(&self) {
        self.window.set_visible(true);
        self.tray.set_visibility(false);
    }

    fn exit(&self) {
        self.stop_threads();
        nwg::stop_thread_dispatch();
    }

}

fn main() {
    nwg::init().expect("Failed to init Native Windows GUI");
    nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font");

    let  _app = GitBackup::build_ui(Default::default()).expect("Failed to build UI");
    nwg::dispatch_thread_events();
} 

Those messages mean that the UI library isn't thread safe which is really common for OS level UI libraries (i assume that this is a Windows UI library because of "winapi").
With the design you currently have this is indeed not possible. A common way to deal with a resource that isn't thread safe is to use a channel. One thread just receives events and "applies" them to the object while other threads can tell it what to do by sending it through the channel.

Note that putting a shared reference in a Mutex is useless. If an Arc<Mutex<...>> is the solution it will generally involve moving ownership of the data inside. However in this case you're dealing with both !Sync and !Send data, so it won't work.

This is likely a restriction inherited from the underlying UI framework, which allows only the main thread to access the UI data.

You'll have to write your threads such that they send a message to the main thread telling it how to update the UI. native_windows_gui seems to have an example somewhat relevant for this native-windows-gui/native-windows-gui/examples/dialog_multithreading_d.rs at master · gabdube/native-windows-gui · GitHub

2 Likes

I have already tried the channel approach. At least the text_box is not moveable, so it runs only in the main thread. So the receiver and the textbox run in the main thread. The two threads run in a loop and send messages to the channel and in the main thread the receiver and the textbox run also in a loop. Problem is that the application freezes because of the for loop. I think that this approach is wrong.

I found the example some times ago, but I haven't understood it. Now I have to check it. Thanks.

FYI I gave it a quick look because I got a bit interested and this is what I understood:

  • you want a field of type nwg::Notice to receive the "callback" from the threads you spawn
  • this field should be annotated with #[nwg_events( OnNotice: [YourStruct::your_callback_method] )] to specify what callback method should be called when the thread stops
  • when you spawn your thread you call self.notice.sender() and give it to the thread. This will be used when the thread wants to notify anything to the UI thread (that it finished, it sent some data to a channel, it wrote some data to a mutex, etc etc)
  • when the callback method is called you can read from the UI thread the data that was written by the other thread, and correctly update your UI in response.

Hi,
thank you fur answer. I have switched to rust-ms. I don't use
native-windows-gui anymore.