저는 그리드에 부착하는 방법을 선택했습니다! 곧바로 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: >k::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구성은 끝났고, 이제 계산기 기능을 구현해야합니다.
gtk-rs도 여타 GUI Framework들과 마찬가지로 Event Loop가 돕니다. UI에 Event Listener를 부착해서 UI의 동작을 코딩하는 것인데, 자세한 시스템은 Event Loop 구글링!
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)입니다.
위 세 가지 수식은 모두 같은 식입니다. 중위 표기법에서 후위 표기법으로 바꾸는 방법은 역시 구글링!
연산을 해주면 됩니다. 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 같은 것으로 검색해보면 될 겁니다...!
각설하고, 지금의 코드는 너무 변변찮기에 말씀드린 에러를 처리해주고 코드를 좀 더 간결하게 만들어야 할 것 같습니다.