Rust로 계산기를 만들어보자! - (Event Listener 부착하기)

apriljade·2022년 5월 24일
0

Entry UI부착


저는 그리드에 부착하는 방법을 선택했습니다! 곧바로 Entry를 생성해서 Gird에 부착하고 기존 버튼들의 행번호를 한칸씩 늘려줍시다!

use gtk4 as gtk;
use gtk::prelude::*;
use glib::clone;
use gtk::glib;

fn main() {
    let application = gtk::Application::new(
        Some("com.github.aprilJade.rs-calculator"),
        Default::default(),
    );

    application.connect_activate(build_ui);
    application.run();
}

fn build_ui(application: &gtk::Application) {
    let window = gtk::ApplicationWindow::new(application);
    window.set_title(Some("Calculator"));

    let grid = gtk::Grid::builder()
        .margin_start(6)
        .margin_end(6)
        .margin_top(6)
        .margin_bottom(6)
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .row_spacing(0)
        .column_spacing(0)
        .build();
    window.set_child(Some(&grid));

    let btn_len = 1;
    let button_0 = gtk::Button::with_label("0");
    let button_1 = gtk::Button::with_label("1");
    let button_2 = gtk::Button::with_label("2");
    let button_3 = gtk::Button::with_label("3");
    let button_4 = gtk::Button::with_label("4");
    let button_5 = gtk::Button::with_label("5");
    let button_6 = gtk::Button::with_label("6");
    let button_7 = gtk::Button::with_label("7");
    let button_8 = gtk::Button::with_label("8");
    let button_9 = gtk::Button::with_label("9");
    let button_clear = gtk::Button::with_label("AC");
    let button_sign = gtk::Button::with_label("+/-");
    let button_percent = gtk::Button::with_label("%");
    let button_divider = gtk::Button::with_label("÷");
    let button_mutiplier = gtk::Button::with_label("x");
    let button_minus = gtk::Button::with_label("-");
    let button_plus = gtk::Button::with_label("+");
    let button_equal = gtk::Button::with_label("=");
    let button_point = gtk::Button::with_label(".");

    let text_view = gtk::Entry::builder()
        .build();

    grid.attach(&text_view, 0, 0, 5, 1);

    grid.attach(&button_clear, 0, 1, btn_len, btn_len);
    grid.attach(&button_sign, 1, 1, btn_len, btn_len);
    grid.attach(&button_percent, 2, 1, btn_len, btn_len);
    grid.attach(&button_divider, 3, 1, btn_len, btn_len);
    
    grid.attach(&button_7, 0, 2, btn_len, btn_len);
    grid.attach(&button_8, 1, 2, btn_len, btn_len);
    grid.attach(&button_9, 2, 2, btn_len, btn_len);
    grid.attach(&button_mutiplier, 3, 2, btn_len, btn_len);
    
    grid.attach(&button_4, 0, 3, btn_len, btn_len);
    grid.attach(&button_5, 1, 3, btn_len, btn_len);
    grid.attach(&button_6, 2, 3, btn_len, btn_len);
    grid.attach(&button_minus, 3, 3, btn_len, btn_len);

    grid.attach(&button_1, 0, 4, btn_len, btn_len);
    grid.attach(&button_2, 1, 4, btn_len, btn_len);
    grid.attach(&button_3, 2, 4, btn_len, btn_len);
    grid.attach(&button_plus, 3, 4, btn_len, btn_len);
    
    grid.attach(&button_0, 0, 5, btn_len * 2, btn_len);
    grid.attach(&button_point, 2, 5, btn_len, btn_len);
    grid.attach(&button_equal, 3, 5, btn_len, btn_len);

    window.show();
}

요렇게하면 UI구성은 끝났고, 이제 계산기 기능을 구현해야합니다.

Event Listner


gtk-rs도 여타 GUI Framework들과 마찬가지로 Event Loop가 돕니다. UI에 Event Listener를 부착해서 UI의 동작을 코딩하는 것인데, 자세한 시스템은 Event Loop 구글링!

Button에 Click event listener 부착하기

gtk-rs는 button click listener는 connect_clicked라는 함수를 통해 구현하는데, gtk에서 제공하는 예제 코드를 참고해서 작성했습니다.

button_1.connect_clicked(clone!(@weak text_view => move |_btn| {
        let text = format!("{}{}", text_view.text(), "1");
        text_view.set_text(text.as_str());
}));

clone! 매크로나 @weak... move... 등등 저를 어지럽게 만드는 것들이 툭툭 튀어나오는데 이건 일단 넘어가고 그 밑에 사용법을 중점적으로 봐주세요. 보시면 아까 만들었던 Entry에서 text()를 이용해서 이미 입력되어있는 텍스트와 문자 "1"을 합친다음 Entry에 다시 그 문자열을 넣어주었습니다. 이런식으로 모든 버튼을 다 작업해주시면 됩니다!

계산 기능 설계


계산기는 단순히 연산만 하는 것이 아니라 연산의 우선순위를 파악, 해당 우선순위 대로 계산해야합니다. 만약 유저가 입력한 수식이 "20 + 5 / 5"라고한다면, 정답은 21이겠지만, 우선순위를 고려하지 않으면 5라는 답이 나올 수도 있습니다. 이런 점을 해결하기 위한 알고리즘 중에 대표적인 것이 수식을 후위 표기법으로 변환 후 연산하는 것입니다.

표기법?

우리가 수학시간에 배우는 표기법은 중위 표기법(infix expression)입니다. 숫자와 숫자 사이에 연산자가 들어가 있는 식이고, 연산자가 앞에 있으면 전위 표기법(prefix expression), 연산자가 뒤에 있으면 후위 표기법(postfix expression)입니다.

  • 전위 표기법: +55
  • 중위 표기법: 5+5
  • 후위 표기법: 55+

위 세 가지 수식은 모두 같은 식입니다. 중위 표기법에서 후위 표기법으로 바꾸는 방법은 역시 구글링!

후위 표기법으로 변환한 이후에는?

연산을 해주면 됩니다. postfix expression의 연산 알고리즘은 역시 구글링!

그래서?

구글링을 해보면 거의 모든 예시가 한 자리 숫자에 한합니다. 두 자리 숫자로 하는 것은 안나와 있어요. (물론 그렇게 샅샅히 검색해보진 않았습니다. 잘 찾아보면 있긴 있을 거에요) 저는 그런 계산기는 필요없기 때문에 한 자리말고 f64 즉, 64bits float형의 범위까지 계산이 가능하도록 만들 생각입니다! 그 이외의 범위는... 아직 고려하지 않겠습니다.
정리를 해보자면,
한 자리 숫자만 처리하는게 아닌 10 이상의 숫자도 처리할 예정이며, 중위 표기법에서 후위 표기법으로 변환한 뒤 변한 후위 표기법에 따라 연산을 처리하는 코드를 작성해야합니다. 후위 표기법으로 변환하는 것이건 연산하는 것이건 전부 Stack이라는 자료구조를 사용하고 숫자, 연산자를 구분해줄 필요가 있습니다.

숫자와 연산자의 구분은 어떻게 해야할까?

만약에 유저가 계산기에 3842+231-255+23이라고 입력을 했다고 가정하겠습니다. 이렇게 되었을 때 숫자와 연산자를 구분하는 방법은 한 글자씩 검사하면서 숫자면 숫자, 연산자면 연산자로 구분하면 됩니다. 이 프로젝트가 C/C++이라면 이렇게 했을테지만 rust에서는 문자열을 한 글자씩 처리하는게 그닥 편하지는 않더군요. 한 글자씩 처리하는 대신 split()이 함수를 활용하기로 했습니다. 숫자와 연산자 사이에 구분자(delimeter)를 넣어주어서 split()만 호출되면 알아서 숫자와 연산자가 구분되게끔 할 수 있습니다.

숫자와 연산자 사이에 구분자는 어떻게 넣을까?

숫자를 입력할 때는 수식에 바로 부착해주면 되고, 연산자를 눌렀을 때는 연산자의 앞 뒤에 구분자가 포함되게 하면 됩니다.

button_plus.connect_clicked(clone!(@weak text_view => move |_btn| {
        let text = format!("{}{}", text_view.text().as_str(), " + ");
        text_view.set_text(text.as_str());
}));

이런 식으로 말이죠.
이렇게하면 3842+231-255+23이 수식을 유저가 입력한다치면 우리의 계산기에는 3842 + 231 - 255 + 23로 입력이 될테고, 이걸 split()을 이용해 공백을 기준으로 짜르면 예쁘게 구분됩니다.

연산하기

스택을 활용하여 구분된 연산자와 숫자를 후위 표기법으로 변환 후 연산까지 진행해주면 완료입니다! 구체적인 방법은 구글링!

마무리를 해야할까...?


만일 여기까지 구현을 하셨다면 어느정도 계산기의 구실을 해낼 수 있는 어플리케이션이 완성 되었을 것입니다. 하지만 0으로 나눈다던가, 숫자가 아닌 문자가 입력되었을 때라던가, 완성된 수식이 아닐 때 계산을 시도한다던가 하는 여러 문제점이 많습니다. 다음 글 부터는 문제를 찾아내는 것과 문제를 해결하는 방법을 위주로 포스팅할 예정입니다.

저런 기능적인 문제말고는 문제가 없을까요...? 저런 문제들을 해결하고 나면 유저 관점으로는 문제가 없어보일 수 있으나 코드 수준으로 살펴보면 문제 덩어리입니다. 제가 올려드리는 코드를 읽으시면 불편하셔야합니다. 하나의 함수로 묶어서 처리할 수 있어보이는 것들이 너무 많고 (== 반복되는 로직이 너무 많고) 저게 최선인가...?하는 의문이 들게 만드는 코드입니다...

사실 제가 주로 사용하는 C/C++이었다면 코드가 더 간결해질 수 있었겠습니다만,,,rust 이놈은 도무지 모르겠습니다. event listener부착하면서 사실 정신이 나갈 것 같았습니다. clone!(@weak text_view => move |_btn| { ...} 이게 도대체 뭘까요? 저도 이유도 모르고 돌아가게 만들긴했는데, 정말 모르겠습니다. 만일 rust에 진지하게 임하시고 계시다면 해당 매크로들과 문법을 세세하게 공부해보세요... rust macro, rust closure 같은 것으로 검색해보면 될 겁니다...!

각설하고, 지금의 코드는 너무 변변찮기에 말씀드린 에러를 처리해주고 코드를 좀 더 간결하게 만들어야 할 것 같습니다.

0개의 댓글