I have made a menu that utilizes the composite design pattern and would like to get some feedback on the code, so be as harsh as you can.
Menu.h:
#ifndef MENU_H
#define MENU_H
#include "Console.h"
#include <functional>
#include <iostream>
#include <memory>
#include <vector>
class ContainerComponent
{
public:
virtual const std::string& getName() const = 0;
virtual void call() = 0;
virtual ~ContainerComponent() = default;
};
class CompositeMenu final : public ContainerComponent
{
public:
void addChild(const std::unique_ptr<ContainerComponent> child);
void removeChild(const std::unique_ptr<ContainerComponent> child);
void addHeader(const std::string& header);
void enableBoxedOptions(int boxHight = 3);
void disableBoxedOptions();
void setMenuXY(int x, int y);
virtual const std::string& getName() const override;
virtual void call() override;
explicit CompositeMenu(int x = 0, int y = 0, bool enabledBoxedMenu = false, int boxHight = 3)
: m_menuXPos{ x }, m_menuYPos{ y }, m_enabledBoxedMenu{ enabledBoxedMenu }
{//boxHight = 3 is the minimal size for symmetry
if (m_enabledBoxedMenu)
{
enableBoxedOptions(boxHight);
}//end of if
}
private:
std::vector<std::unique_ptr<ContainerComponent> > m_children;
std::string m_header{};
bool m_enabledBoxedMenu;
bool m_enabledHeader{ false };
int m_menuXPos;
int m_menuYPos;
unsigned int m_menuHight;
unsigned int m_menuLength;
unsigned int m_jumpRange{ 1 };
void updateLengths(const std::string& opt, int padding);
void printBoxedOptions(int& curXPos, int& curYPos) const;
void printBareOptions(int& curXPos, int& curYPos) const;
void printHeader(int& curXPos, int& curYPos) const;
void printMenu(int curXPos, int curYPos) const;
void executeOption(intPair coords) const;
void updateMenuHight();
int getPadding() const;
};
class Action final : public ContainerComponent
{
public:
virtual const std::string& getName() const override;
virtual void call() override;
explicit Action(const std::string& actionName,
const std::function<void()> action)
: m_actionName{ actionName }, m_action{ action }
{
}
private:
const std::function<void()> m_action;
const std::string m_actionName;
};
#endif
Menu.cpp:
#include "Menu.h"
#include <string>
#include <algorithm>
namespace
{
unsigned constexpr char selectionArrow { 175 };
constexpr int boxedLengthPadding{ 4 };
constexpr int boxedHightPadding { 2 };
constexpr int yRangePadding { 2 };
constexpr int bareLengthPadding { 1 };
constexpr int xJumpRange { 0 };
int findMiddle(int num)
{
return ((num / 2) + 1);
}
}
/*|---COMPOSITE_MENU_FUNCTIONS_START---|*/
/*|---PUBLIC_FUNCTIONS_START---|*/
void CompositeMenu::addChild(const std::unique_ptr<ContainerComponent> child)
{
if (std::find_if(m_children.begin(),
m_children.end(),
[&child](const std::unique_ptr<ContainerComponent> cmp)
{return cmp->getName() == child->getName(); }) == m_children.end())
{
m_children.emplace_back(child);
if (m_enabledBoxedMenu) updateLengths(child->getName(), boxedLengthPadding);
else updateLengths(child->getName(), bareLengthPadding);
}//end of if
else std::cout << "An Action with this name already exists.\n";
}
void CompositeMenu::removeChild(const std::unique_ptr<ContainerComponent> child)
{
if (std::find_if(m_children.begin(),
m_children.end(),
[&child](const std::unique_ptr<ContainerComponent> cmp)
{return cmp.get() == child.get(); }) != m_children.end())
{
m_children.erase(std::remove_if(m_children.begin(),
m_children.end(),
[&child](const std::unique_ptr<ContainerComponent> cmp)
{return cmp.get() == child.get(); }),
m_children.end());
}//end of if
else std::cout << "The option doesn't exist.\n";
}
void CompositeMenu::addHeader(const std::string& header)
{
m_enabledHeader = true;
m_header = header;
if (static_cast<unsigned int>(m_header.length()) + 1 > m_menuLength)
{
m_menuLength = static_cast<unsigned int>(m_header.length()) + 1;
}//end of if
}
void CompositeMenu::enableBoxedOptions(int boxHight)
{//must be an odd number for symmetry reasons
if ((boxHight >= 3) && (boxHight % 2 != 0))
{
m_enabledBoxedMenu = true;
m_jumpRange = boxHight;
}//end of if
else std::cout << "Invalid box size.\n";
}
void CompositeMenu::disableBoxedOptions()
{
if (m_enabledBoxedMenu)
{
m_enabledBoxedMenu = false;
m_jumpRange = 1; //default jump range with no boxes
updateMenuHight();
}//end of if
}
void CompositeMenu::setMenuXY(int x, int y)
{
Console::setXY(x, y, m_menuXPos, m_menuYPos);
}
/*|---VIRTUAL_FUNCTIONS_START---|*/
const std::string& CompositeMenu::getName() const
{
return m_header;
}
void CompositeMenu::call()
{
if (static_cast<int>(m_children.size()) == 0)
std::cout << "No options found, unable to print menu.\n";
else
{
updateMenuHight();
printMenu(m_menuXPos, m_menuYPos);
executeOption(Console::navigation(intPair(xJumpRange, m_jumpRange),
intPair(m_menuXPos + 1, m_menuXPos + 1),
intPair(m_menuYPos + getPadding(),
(m_menuYPos + getPadding()) + m_jumpRange * (static_cast<int>(m_children.size()) - 1)),
selectionArrow));
}//end of else
}
/*|----VIRTUAL_FUNCTIONS_END----|*/
/*|----PUBLIC_FUNCTIONS_END----|*/
/*|---PRIVATE_FUNCTIONS_START---|*/
void CompositeMenu::updateLengths(const std::string& opt, int padding)
{
if ((static_cast<unsigned int>(opt.length()) + padding) > m_menuLength)
{
m_menuLength = static_cast<unsigned int>(opt.length()) + padding;
}//end of if
}
void CompositeMenu::printBoxedOptions(int& curXPos, int& curYPos) const
{
for (size_t index{}; index < m_children.size(); ++index)
{
Console::drawBox(m_jumpRange - boxedHightPadding, m_menuLength - boxedLengthPadding, curXPos, curYPos);
Console::gotoxy(curXPos + 2, curYPos + findMiddle(m_jumpRange) - 1);
std::cout << m_children[index]->getName();
curYPos += m_jumpRange;
}//end of for
}
void CompositeMenu::printBareOptions(int& curXPos, int& curYPos) const
{
++curXPos;
for (size_t index{}; index < m_children.size(); ++index)
{
Console::gotoxy(curXPos, curYPos++);
std::cout << m_children[index]->getName();
}//end of for
}
void CompositeMenu::printHeader(int& curXPos, int& curYPos) const
{
Console::gotoxy(curXPos + 1, curYPos);
std::cout << m_header;
Console::gotoxy(curXPos + 1, ++curYPos);
for (size_t index{}; index < m_header.length(); ++index) std::cout << '-';
++curYPos;
}
void CompositeMenu::printMenu(int curXPos, int curYPos) const
{
Console::drawBox(m_menuHight, m_menuLength, curXPos, curYPos);
curXPos += 2;
++curYPos;
if (m_enabledHeader) printHeader(curXPos, curYPos);
if (m_enabledBoxedMenu) printBoxedOptions(curXPos, curYPos);
else printBareOptions(curXPos, curYPos);
}
void CompositeMenu::executeOption(intPair coords) const
{
int destX, destY;
if (m_enabledHeader) destX = m_menuXPos + m_menuLength - 1;
else destX = m_menuXPos + m_menuLength;
if (m_enabledHeader) destY = m_menuYPos + m_menuHight - 3;
else destY = m_menuYPos + m_menuHight;
Console::clrSec(m_menuXPos, m_menuYPos, destX, destY);
m_children[(coords.second - (m_menuYPos + getPadding())) / m_jumpRange]->call();
}
void CompositeMenu::updateMenuHight()
{
if (m_enabledHeader) m_menuHight = m_jumpRange * static_cast<unsigned int>(m_children.size()) + 2;
else m_menuHight = m_jumpRange * static_cast<unsigned int>(m_children.size());
}
int CompositeMenu::getPadding() const
{
if (m_enabledHeader) return(findMiddle(m_jumpRange) + yRangePadding);
else return(findMiddle(m_jumpRange));
}
/*|----PRIVATE_FUNCTIONS_END----|*/
/*|----COMPOSITE_MENU_FUNCTIONS_END----|*/
/*|---ACTION_FUNCTIONS_START---|*/
/*|---PUBLIC_FUNCTIONS_START---|*/
/*|---VIRTUAL_FUNCTIONS_START---|*/
const std::string& Action::getName() const
{
return m_actionName;
}
void Action::call()
{
m_action();
}
/*|----VIRTUAL_FUNCTIONS_END----|*/
/*|----PUBLIC_FUNCTIONS_END----|*/
/*|----ACTION_FUNCTIONS_END----|*/
I will also add the Console.h library that I wrote if anyone wants to use it:
Console.h:
#ifndef CONSOLE_GRAPHICS_H
#define CONSOLE_GRAPHICS_H
#include <utility>
using intPair = std::pair<int, int>;
namespace Console
{
intPair navigation(const intPair& jumpLength,
const intPair& xRange,
const intPair& yRange,
unsigned const char symbol);
void setXY(int destX, int destY, int &curX, int &curY);
void putSymbol(int xPos, int yPos, const char symbol);
void clrSec(int curX, int curY, int destX, int destY);
void drawBox(int hight, int length, int x, int y);
void gotoxy(int x, int y);
void clrScr();
}
#endif
Console.cpp:
#include "Constants.h"
#include "Console.h"
#include <iostream>
#include <Windows.h>
#include <conio.h>
namespace
{
constexpr int downKey { 80 };
constexpr int rightKey{ 77 };
constexpr int leftKey { 75 };
constexpr int upKey { 72 };
unsigned constexpr char enterKey{ 13 };
constexpr int padding { 2 };
}
intPair Console::navigation(const intPair& jumpLength,
const intPair& xRange,
const intPair& yRange,
unsigned const char symbol)
{
int curX{ xRange.first };
int curY{ yRange.first };
do {
putSymbol(curX, curY, symbol);
gotoxy(curX, curY);
if (_getch() == enterKey) return intPair(curX, curY);
switch (static_cast<int>(_getch()))
{
case upKey:
{
if ((curY - jumpLength.second) < yRange.first)
{
curY = yRange.second;
putSymbol(curX, yRange.first, ' ');
}//end of if
else
{
curY -= jumpLength.second;
putSymbol(curX, curY + jumpLength.second, ' ');
}//end of else
}//end of case
break;
case downKey:
{
if ((curY + jumpLength.second) > yRange.second)
{
curY = yRange.first;
putSymbol(curX, yRange.second, ' ');
}//end of if
else
{
curY += jumpLength.second;
putSymbol(curX, curY - jumpLength.second, ' ');
}//end of else
}//end of case
break;
case leftKey:
{
if ((curX - jumpLength.first) < xRange.first)
{
curX = xRange.second;
putSymbol(xRange.first, curY, ' ');
}//end of if
else
{
curX -= jumpLength.first;
putSymbol(curX + jumpLength.first, curY, ' ');
}//end of else
}//end of case
break;
case rightKey:
{
if ((curX + jumpLength.first) > xRange.second)
{
curX = xRange.first;
putSymbol(xRange.second, curY, ' ');
}//end of if
else
{
curX += jumpLength.first;
putSymbol(curX - jumpLength.first, curY, ' ');
}//end of else
}//end of case
break;
}//end of switch
} while (true);
}
void Console::setXY(int destX, int destY, int &curX, int &curY)
{
if (destX < 0)
{
curX = -destX;
std::cout << "X value was made positive.\n";
}//end of if
else curX = destX;
if (destY < 0)
{
curY = -destY;
std::cout << "Y value was made positive.\n";
}//end of if
else curY = destY;
}
inline void Console::putSymbol(int xPos, int yPos, const char symbol)
{
gotoxy(xPos, yPos);
std::cout << symbol;
}
void Console::clrSec(int curX, int curY, int destX, int destY)
{
gotoxy(curX, curY);
for (int row{}; row < destY; ++row)
for (int col{}; col < destX; ++col)
{
Console::gotoxy(curX + col, curY + row);
std::cout << ' ';
}//end of for
gotoxy(curX, curY);
}
void Console::drawBox(int hight, int length, int x, int y)
{
gotoxy(x, y);
std::cout << Shapes::topLeftAngle;
for (int i{}; i < (length + padding); ++i) std::cout << Shapes::horizontalPiece;
std::cout << Shapes::topRightAngle;
for (int i{}; i < hight; ++i)
{
gotoxy(x, ++y);
std::cout << Shapes::verticalPiece;
gotoxy(x + length + 3, y); // x + length + 3 represents the furthest boarder
std::cout << Shapes::verticalPiece;
}//end of for
gotoxy(x, ++y);
std::cout << Shapes::bottomLeftAngle;
for (int i{}; i < (length + padding); ++i) std::cout << Shapes::horizontalPiece;
std::cout << Shapes::bottomRightAngle;
}
inline void Console::gotoxy(int x, int y)
{
COORD pos = { static_cast<short>(x), static_cast<short>(y) };
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
void Console::clrScr()
{
CONSOLE_SCREEN_BUFFER_INFO csbi;
HANDLE hStdOut;
DWORD count;
DWORD cellCount;
COORD homeCoords = { 0, 0 };
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdOut == INVALID_HANDLE_VALUE) return;
if (!GetConsoleScreenBufferInfo(hStdOut, &csbi)) return;
cellCount = csbi.dwSize.X *csbi.dwSize.Y;
if (!FillConsoleOutputCharacter(
hStdOut,
(TCHAR) ' ',
cellCount,
homeCoords,
&count
)) return;
SetConsoleCursorPosition(hStdOut, homeCoords);
}
Constants.h:
#ifndef CONSTANTS_H
#define CONSTANTS_H
namespace Shapes
{
extern unsigned const char bottomRightAngle;
extern unsigned const char topRightAngle;
extern unsigned const char bottomLeftAngle;
extern unsigned const char topLeftAngle;
extern unsigned const char horizontalPiece;
extern unsigned const char verticalPiece;
}
#endif
Constants.cpp:
#include "Constants.h"
namespace Shapes
{
extern unsigned const char bottomRightAngle{ 188 };
extern unsigned const char topRightAngle { 187 };
extern unsigned const char bottomLeftAngle { 200 };
extern unsigned const char topLeftAngle { 201 };
extern unsigned const char horizontalPiece { 205 };
extern unsigned const char verticalPiece { 186 };
}
Note that the method used for Constants.h is in place because it is meant to be included in a bunch of files and I don't want the constants to be defined in each separate file all over again.
Lastly, here is the source.cpp I used for testing:
#include "Menu.h"
#include "Console.h"
void a() { std::cout << 'a'; }
void b() { std::cout << 'b'; }
void c() { std::cout << 'c'; }
void d() { std::cout << 'd'; }
int main()
{
std::unique_ptr<CompositeMenu> menu{ std::make_unique<CompositeMenu>(5, 5, true) };
std::unique_ptr<CompositeMenu> submenu{ std::make_unique<CompositeMenu>(5, 5) };
submenu->addHeader("Sub menu");
menu->addHeader("Main menu");
std::unique_ptr<ContainerComponent> optA{ std::make_unique<Action>("a", a) };
std::unique_ptr<ContainerComponent> optB{ std::make_unique<Action>("b", b) };
std::unique_ptr<ContainerComponent> optC{ std::make_unique<Action>("c", c) };
std::unique_ptr<ContainerComponent> optD{ std::make_unique<Action>("d", d) };
menu->addChild(std::move(optA));
menu->addChild(std::move(optB));
menu->addChild(std::move(optC));
menu->addChild(std::move(optD));
menu->removeChild(std::move(optB));
submenu->addChild(std::move(optA));
submenu->addChild(std::move(optB));
submenu->addChild(std::move(optC));
submenu->addChild(std::move(optD));
menu->addChild(std::move(submenu));
menu->call();
return 0;
}
const unsigned char
overunsigned const char
. Theconst
is likely to be overlooked \$\endgroup\$using intPair = std::pair<int, int>;
actually helps readability. I would go without it. \$\endgroup\$