Stworzyłem interfejs menu w C++

Stworzyłem interfejs menu w C++
whiteman808
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 201
0

Hej, napisałem prosty interfejs menu w C++. Co sądzicie o nim? Co mogę w nim poprawić? Pozdrawiam, whiteman808

Kopiuj
~/programy_cpp/linked_list ❯ cat main.cpp                                                                                                             17:30:52
Kopiuj
// the purpose of the below code is to test menu class

#include <ios>
#include <iostream>
#include <limits>
#include <string>
#include <utility>
#include "menu.h"

/*
 * these functions are defined only to test menu.h library
 * i plan to merge these functions to the CalculatorApp class
 * in the future
 *
 * i wrote below code in a hurry
 */

// helper functions; will be moved into utils.h/utils.cpp in the future
inline void clear_console_input() {
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

double read_double(const std::string& prompt = "Enter the number: ") {
    double number;
    bool success{false};
    do {
        std::cout << prompt;
        if (std::cin >> number) {
            success = true;
        } else {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            std::cout << "Error: not a number." << std::endl;
        }
    } while (!success);
    return number;
}

std::pair<double, double> read_operands() {
    double first{read_double("Enter the first operand: ")};
    double second{read_double("Enter the second operand: ")};
    clear_console_input();
    return std::make_pair(first, second);
}

std::pair<double, double> read_operands_div() {
    double first{read_double("Enter the first operand: ")};
    double second;
    while ((second = read_double("Enter the second operand: ")) == 0) {
        std::cout << "Error: number is equal to zero." << std::endl;
    }
    clear_console_input();
    return std::make_pair(first, second);
}

// calculator interface
void add() {
    auto numbers = read_operands();
    std::cout << "Result of " << numbers.first << " + " << numbers.second
              << " is " << numbers.first + numbers.second << std::endl;
}

void subtract() {
    auto numbers = read_operands();
    std::cout << "Result of " << numbers.first << " - " << numbers.second
              << " is " << numbers.first - numbers.second << std::endl;
}

void multiply() {
    auto numbers = read_operands();
    std::cout << "Result of " << numbers.first << " * " << numbers.second
              << " is " << numbers.first * numbers.second << std::endl;
}

void divide() {
    auto numbers = read_operands_div();
    std::cout << "Result of " << numbers.first << " / " << numbers.second
              << " is " << numbers.first / numbers.second << std::endl;
}

int main() {
    menu app{"Calculator"};
    app.add_item("Add", add);
    app.add_item("Subtract", subtract);
    app.add_item("Multiply", multiply);
    app.add_item("Divide", divide);
    menu menu2{"test"};
    app.add_submenu(menu2);
    app.loop();
    std::cout << "Good bye!\n";
    return 0;
}
Kopiuj
~/programy_cpp/linked_list ❯ cat menu.h                                                                                                               17:30:55
Kopiuj
#ifndef MENU_H
#define MENU_H

#include <functional>
#include <iostream>
#include <string>
#include <vector>

struct menu_item {
    explicit menu_item(const std::string& title_val) : title{title_val}, action{noop} {}
    menu_item(const std::string& title_val, std::function<void()> action_val)
        : title{title_val}, action{action_val} {}
    menu_item(const menu_item& item) : title{item.title}, action{item.action} {}

    friend std::ostream& operator<<(std::ostream& lhs, const menu_item& rhs);

    static void noop() { std::cout << "No action defined!\n"; }

    std::string title{};
    std::function<void()> action{};
};

class menu {
public:
    explicit menu(const std::string& title_val);
    menu(const std::string& title_val, const std::vector<menu_item>& items_val);
    menu(const std::string& title_val, const std::string& description_val);
    menu(const std::string& title_val, const std::string& description_val,
         const std::vector<menu_item>& items_val);

    void add_item(const menu_item& item);
    void add_item(const std::string& title_val, std::function<void()> action);
    void add_item_at(int index, const menu_item& item);
    void add_item_at(int index, const std::string& title_val,
                     std::function<void()> action);
    void remove_item(int index);

    void add_submenu(const menu& menu_val);
    void add_submenu(const std::string& title_val, const menu& menu_val);
    void add_submenu_at(int index, const menu& menu_val);
    void add_submenu_at(int index, const std::string& title_val,
                        const menu& menu_val);

    void set_title(const std::string& title_val);
    void set_description(const std::string& description_val);

    void loop() const;

private:
    // helper functions
    void print_choices() const;
    char read_choice() const;
    void check_index(std::size_t index) const;

    // wrapper function for menu::loop
    std::function<void()> invoke_menu_loop(const menu& menu_val);

    static int nesting_level;
    std::string title{};
    std::string description{};
    std::vector<menu_item> items{};
};

#endif // MENU_H
Kopiuj
~/programy_cpp/linked_list ❯ cat menu.cpp                                                                                                             17:30:58
Kopiuj
#include "menu.h"
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

static constexpr char quit_choice = 'q';

// helper functions
static void print_title(const std::string& title) {
    std::size_t title_len = title.size();
    std::size_t width = title_len + 4;
    for (std::size_t i = 0; i < width; ++i) {
        std::cout << "*";
    }
    std::cout << "\n* " << title << " *\n";
    for (std::size_t i = 0; i < width; ++i) {
        std::cout << "*";
    }
    std::cout << std::endl;
}

// menu_item struct i/o operators
std::ostream& operator<<(std::ostream& lhs, const menu_item& rhs) {
    return lhs << rhs.title;
}

// menu class constructors
menu::menu(const std::string& title_val) : title{title_val} {}

menu::menu(const std::string& title_val,
           const std::vector<menu_item>& items_val)
    : title{title_val}, items{items_val} {}

menu::menu(const std::string& title_val, const std::string& description_val)
    : title{title_val}, description{description_val} {}

menu::menu(const std::string& title_val, const std::string& description_val,
           const std::vector<menu_item>& items_val)
    : title{title_val}, description{description_val}, items{items_val} {}

// menu class static variables
int menu::nesting_level{0};

// menu class public interface
void menu::add_item(const menu_item& item) { items.push_back(item); }

void menu::add_item(const std::string& title_val, std::function<void()> action_val) {
    items.push_back(menu_item{title_val, action_val});
}

void menu::add_item_at(int index, const menu_item& item) {
    check_index(index);
    items.insert(items.begin() + index, item);
}

void menu::add_item_at(int index, const std::string& title_val,
                       std::function<void()> action) {
    check_index(index);
    items.insert(items.begin() + index, menu_item{title_val, action});
}

void menu::remove_item(int index) {
    check_index(index);
    items.erase(items.begin() + index);
}

void menu::add_submenu(const menu& menu_val) {
    items.push_back(menu_item{menu_val.title, invoke_menu_loop(menu_val)});
}

void menu::add_submenu(const std::string& title_val, const menu& menu_val) {
    items.push_back(menu_item{title_val, invoke_menu_loop(menu_val)});
}

void menu::add_submenu_at(int index, const menu& menu_val) {
    check_index(index);
    items.insert(items.begin() + index,
                 menu_item{menu_val.title, invoke_menu_loop(menu_val)});
}

void menu::add_submenu_at(int index, const std::string& title_val,
                          const menu& menu_val) {
    check_index(index);
    items.insert(items.begin() + index,
                 menu_item{title_val, invoke_menu_loop(menu_val)});
}

void menu::set_title(const std::string& title_val) { title = title_val; }

void menu::set_description(const std::string& description_val) {
    description = description_val;
}

void menu::loop() const {
    bool active = true;
    ++nesting_level;
    while (active) {
        print_title(title);
        if (!description.empty()) {
            std::cout << std::endl;
            std::cout << description;
            std::cout << std::endl;
        }
        char choice = read_choice();
        if (choice == quit_choice) {
            --nesting_level;
            active = false;
        } else {
            std::cout << '\n';
            // call function bound to menu item
            items[choice - '0'].action();
            // wait until user press the enter key
            std::cout << "\nPress enter to continue...\n";
            std::cin.get();
        }
    }
}

// menu class private functions
void menu::print_choices() const {
    for (std::size_t i = 0; i < items.size(); ++i) {
        std::cout << i << ") " << items[i] << std::endl;
    }
    std::cout << "q) ";
    if (nesting_level > 1)
        std::cout << "Back";
    else
        std::cout << "Quit";
    std::cout << std::endl;
}

char menu::read_choice() const {
    std::string user_input;
    char choice;
    bool valid{false};

    std::cout << "What do you want to do?\n";
    do {
        print_choices();
        std::cout << "Your choice: ";
        std::getline(std::cin, user_input);
        if (user_input.length() > 1 || user_input.empty()) {
            std::cout << "Please enter the valid choice.\n";
            continue;
        }
        choice = tolower(user_input.at(0));
        if ((choice - '0') < 0 ||
            (static_cast<std::size_t>(choice - '0') >= items.size() &&
             (choice != quit_choice))) {
            std::cout << "Invalid choice!\n";
        } else {
            valid = true;
        }
    } while (!valid);
    return choice;
}

void menu::check_index(std::size_t index) const {
    const std::size_t max_allowed_idx = items.size() - 2;
    if (index > max_allowed_idx) {
        std::ostringstream oss;
        oss << "index value must be between " << index << " and "
            << max_allowed_idx;
        throw std::out_of_range{oss.str()};
    }
}

// wrapper function for menu::loop
std::function<void()> menu::invoke_menu_loop(const menu& the_menu) {
    return [&the_menu]() { the_menu.loop(); };
}
Spine
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 6968
0

Twój kod jest naprawdę dobrze napisany i przemyślany! Masz czytelną strukturę, oddzielenie funkcji pomocniczych, a także odpowiednią enkapsulację w klasie menu. Podoba mi się, że menu obsługuje zarówno elementy akcji, jak i podmenu, co zwiększa jego elastyczność.

Co można poprawić?

1. Poprawienie indeksowania wyborów menu

  • W funkcji read_choice() użytkownik wprowadza numery zaczynające się od 0, co może być trochę nieintuicyjne. Standardowo w interfejsach użytkownika wybory zaczynają się od 1. Możesz dodać +1 przy wyświetlaniu opcji i odpowiednio zmodyfikować obsługę wejścia.

Poprawka:

Kopiuj
std::cout << (i + 1) << ") " << items[i] << std::endl;

Następnie w read_choice():

Kopiuj
if ((choice - '1') < 0 || (static_cast<std::size_t>(choice - '1') >= items.size() && (choice != quit_choice))) {

2. Obsługa błędnych indeksów w loop()

  • items[choice - '0'].action(); zakłada, że choice - '0' jest poprawnym indeksem. Warto dodać dodatkową walidację:
Kopiuj
std::size_t index = choice - '0';
if (index >= items.size()) {
    std::cout << "Invalid choice!\n";
    continue;
}

3. Usprawnienie funkcji check_index()

  • W tej metodzie masz stałe ograniczenie max_allowed_idx = items.size() - 2;, które wydaje się błędne. Powinieneś sprawdzać, czy index mieści się w items.size() - 1.

4. Lepsza obsługa błędów wejścia

  • Możesz napisać ogólną funkcję read_input<T>(), która obsługuje błędy zamiast powtarzać ten sam fragment kodu.
Kopiuj
template <typename T>
T read_input(const std::string& prompt) {
    T value;
    while (true) {
        std::cout << prompt;
        if (std::cin >> value) break;
        std::cout << "Invalid input. Try again.\n";
        std::cin.clear();
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }
    return value;
}

Teraz można uprościć np. read_double():

Kopiuj
double read_double(const std::string& prompt = "Enter a number: ") {
    return read_input<double>(prompt);
}

5. Lepsza obsługa std::function

  • Twój sposób przechowywania funkcji w menu_item działa, ale można go zoptymalizować przez użycie std::move().
Kopiuj
menu_item(const std::string& title_val, std::function<void()> action_val)
    : title{title_val}, action{std::move(action_val)} {}

6. Możliwość dynamicznej zmiany funkcji w menu_item

  • Może być przydatne dodanie setterów do zmiany funkcji akcji bez konieczności tworzenia nowego elementu.
Kopiuj
void set_action(std::function<void()> new_action) {
    action = std::move(new_action);
}

Podsumowanie

Twój kod jest już bardzo solidny, ale można go jeszcze bardziej ulepszyć:

  • Zmienione indeksowanie menu (bardziej intuicyjne dla użytkownika).
  • Lepsza obsługa błędnych wyborów (sprawdzenie indeksów przed użyciem).
  • Generyczna funkcja do wczytywania danych (zapobiega duplikacji kodu).
  • Lepsze wykorzystanie std::move() w konstruktorach.
  • Opcja dynamicznej zmiany funkcji w menu_item.

Poza tym kod jest bardzo dobrze zorganizowany i łatwy w rozszerzaniu. Świetna robota! 🚀

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.