I'm trying to write an OO menu system for a game, loosely based on the idea of a Model,View,Controller. In my app so far I've named the views "renderers" and the models are without a suffix. I created a generic menu class which stores the items of a menu, which are menu_item objects, and there is also a menu renderer class which creates renderers for each item and renders them. The problem is I'm not sure where to store the data and logic to do with where on the screen each item should be positioned, and how to check if it is being hovered over, etc. My original idea was to store and set a selected property on each menu item, which could be rendered differently by different views, but even then how to I deal with positioning the graphical elements that make up the button?
Code excerpts so far follow: (more code at https://gist.github.com/3422226)
/**
* Abstract menu model
*
* Menus have many items and have properties such as a title
*/
class menu {
protected:
std::string _title;
std::vector<menu_item*> _items;
public:
std::string get_title();
void set_title(std::string);
std::vector<menu_item*> get_items();
};
class menu_controller: public controller {
private:
menu* _menu;
public:
menu_controller(menu*);
virtual void update();
};
class menu_item {
protected:
std::string _title;
public:
menu_item(std::string title);
virtual ~menu_item();
std::string get_title();
};
class menu_renderer: public renderer {
private:
menu* _menu;
bitmap _background_bitmap;
static font _title_font;
std::map<menu_item*, menu_item_renderer*> _item_renderers;
public:
menu_renderer(menu*);
virtual void render();
};
font menu_renderer::_title_font = NULL;
menu_renderer::menu_renderer(menu* menu) {
_menu = menu;
_background_bitmap = ::load_bitmap("blackjack_menu_bg.jpg");
if (!_title_font)
_title_font = ::load_font("maven_pro_regular.ttf",48);
}
void menu_renderer::render() {
::draw_bitmap(_background_bitmap, 0, 0);
/* Draw the menu title */
const char* title = _menu->get_title().c_str();
int title_width = ::text_width(_title_font, title);
::draw_text(title, color_white, _title_font, screen_width() - title_width - 20, 20);
/* Render each menu item */
std::vector<menu_item*> items = _menu->get_items();
for (std::vector<menu_item*>::iterator it = items.begin(); it != items.end(); ++it) {
menu_item* item = *it;
if (!_item_renderers.count(item))
_item_renderers[item] = new menu_item_renderer(item, it - items.begin());
_item_renderers[item]->render();
}
}
class menu_item_renderer: public renderer {
private:
unsigned _order;
menu_item* _item;
static font _title_font;
public:
menu_item_renderer(menu_item*, unsigned);
virtual ~menu_item_renderer();
virtual void render();
};
font menu_item_renderer::_title_font = NULL;
menu_item_renderer::menu_item_renderer(menu_item* item, unsigned order) {
_item = item;
_order = order;
if (!_title_font)
_title_font = ::load_font("maven_pro_regular.ttf",24);
}
menu_item_renderer::~menu_item_renderer() {
// TODO Auto-generated destructor stub
}
void menu_item_renderer::render() {
const char* title = _item->get_title().c_str();
int title_width = ::text_width(_title_font, title);
unsigned y = 44 * _order + 20;
::fill_rectangle(color_red, 20, y, title_width + 40, 34);
::draw_text(title, color_white, _title_font, 30, y + 5);
}
Your Model class menu needs a add_view(menuview *v) and update() method.
Then you can delegate the update of your widget to the derived View (menu_renderer or a cli_menu_renderer).
The Controller needs to know the Model (as member), when the Controller runs (or executes a Command) and has to update the Model with a Setter (like m_menu_model->set_selected(item, state)) and the Model calls update() on Setters.
Your Controller menu_controller has a update method, there you could also ask for Input, like if (menuview->toggle_select()) m_menu_model->toggle_selected(); (which all menuviews have to implement) and invoke the setter, but thats a inflexible coupling of View and Controller (you could check MVC with the Command Pattern for a more advanced combination).
For the Position you can set member vars like int m_x, m_y, m_w, m_h.
But these members are specific to a View with GUI, so only the derived View needs them.
Then you could use these values to compare against mouse Positions and use a MouseOver Detection Method like this:
// View menu_item
bool menu_item::over()
{
if (::mouse_x > m_x
&& ::mouse_x < m_x + m_w
&& ::mouse_y > m_y
&& ::mouse_y < m_y + m_h) {
return true;
}
return false;
}
// update on gui menu item
bool menu_item::update()
{
if (over()) {
m_over = true;
}
else {
m_over = false;
}
// onclick for the idea
if ((::mouse_b & 1) && m_over) {
// here you could invoke a callback or fire event
m_selected = 1;
} else {
m_selected = 0;
}
return m_selected;
}
// update the command line interface menu item
bool cli_menu_item::update()
{
if ((::enterKeyPressed & 1) && m_selected) {
// here you could invoke a callback or fire event
m_selected = 1;
} else {
m_selected = 0;
}
return m_selected;
}
void menu_item_renderer::render() {
// update widgets
_item->update();
// ...
}
// Model
void menu::add_view(menuview *v) {
m_view=v;
}
void menu::update() {
if (m_view) m_view->update();
}
bool menu::set_selected(int item, int state) {
m_item[index]=state;
update();
}
Related
I have a function that creates a button dynamically
void createBtn(News obj,TForm *Form1){
TButton *spam = new TButton(Form1);
spam->Parent = newsCard;
spam->Position->X = 280;
spam->Position->Y = 256;
spam->Text = "Spam";
}
I need to assign an OnClick event to it, so I added the following line to the function above:
spam->OnClick = spamClick;
The code for spamClick is:
void __fastcall TForm1::spamClick(TObject *Sender, News obj)
{
allUsers[userIndex].spamNews(obj);
ShowMessage("Done!");
}
The problem is, I need to pass the obj in the line, but the function requires 2 arguments, which are the Sender and the obj.
spam->OnClick = spamClick(obj); // error here
But I do not know what to pass. I've tried Form1, spam, and newsCard. But nothing works.
What do I pass as a Sender? Or, is there a way to assign an OnClick event to the button inside createBtn()?
Edit:
class News has the following definition
class News
{
public:
News(string, string, string, Dates);
string title;
string description;
Dates date;
int rate;
string category;
vector<Comment> comments;
int spamCount;
static int newsCount;
int newsID;
int numOfRatedUsers;
};
and spamNews is a function in the user class that pushes the obj.newsID into a vector in the user then increases the spamCount.
void user::spamNews(News& obj) {
//check that the news in not already spammed
if(!findNews(spammedNews,obj)){
spammedNews.push_back(obj.newsID);
obj.spamCount++;
}
}
Your second approach doesn't work, because you are trying to call spamClick() first and then assign its return value to the OnClick event.
Your first approach is the correct way, however you can't add parameters to the OnClick event handler.
TButton has Tag... properties for holding user-defined data. However, since the News object is not being passed around by pointer, the Tag... properties are not very helpful in this case (unless the News object is held in an array/list whose index can then be stored in the Tag).
Otherwise, I would suggest deriving a new class from TButton to hold the News object, eg:
class TMyButton : public TButton
{
public:
News NewsObj;
__fastcall TMyButton(TComponent *Owner, const News &obj)
: TButton(Owner), NewsObj(obj) {}
};
void TForm1::createBtn(const News &obj)
{
TMyButton *spam = new TMyButton(this, obj);
spam->Parent = newsCard;
spam->Position->X = 280;
spam->Position->Y = 256;
spam->Text = _D("Spam");
spam->OnClick = &spamClick;
}
void __fastcall TForm1::spamClick(TObject *Sender)
{
MyButton *btn = static_cast<TMyButton*>(Sender);
allUsers[userIndex].spamNews(btn->NewsObj);
ShowMessage(_D("Done!"));
}
UPDATE: Since your News objects are being stored in a vector that you are looping through, then a simpler solution would be to pass the News object to createBtn() by reference and then store a pointer to that object in the TButton::Tag property, eg:
void TForm1::createBtn(News &obj)
{
TButton *spam = new TButton(this);
spam->Parent = newsCard;
spam->Position->X = 280;
spam->Position->Y = 256;
spam->Text = _D("Spam");
spam->Tag = reinterpret_cast<NativeInt>(&obj);
spam->OnClick = &spamClick;
}
void __fastcall TForm1::spamClick(TObject *Sender)
{
TButton *btn = static_cast<TButton*>(Sender);
News *obj = reinterpret_cast<News*>(btn->Tag);
allUsers[userIndex].spamNews(*obj);
ShowMessage(_D("Done!"));
}
Or, using the TMyButton descendant:
class TMyButton : public TButton
{
public:
News *NewsObj;
__fastcall TMyButton(TComponent *Owner)
: TButton(Owner) {}
};
void TForm1::createBtn(News &obj)
{
TMyButton *spam = new TMyButton(this);
spam->Parent = newsCard;
spam->Position->X = 280;
spam->Position->Y = 256;
spam->Text = _D("Spam");
spam->NewsObj = &obj;
spam->OnClick = &spamClick;
}
void __fastcall TForm1::spamClick(TObject *Sender)
{
MyButton *btn = static_cast<TMyButton*>(Sender);
allUsers[userIndex].spamNews(*(btn->NewsObj));
ShowMessage(_D("Done!"));
}
I want to click a button to open the file browser, open an audio file(mp3) which gets added to a table. I want it to be so that you can have multiple audio files available on the playlist table. I have some of it figured out, but I'm trying to create an event listener where I click a button, 'Add a track'. How can I populate my table or 'playlist' with the audio file? I have a file chooser already implemented that loads the file to be played and a play button that play a loaded file, I'm just not sure how to add it to the table? Any help is appreciated.
My code so far goes something along the lines of:
Array<File> trackTitles;
...
void PlaylistComponent::buttonClicked(Button* button)
{
if (button == &loadButton)
{
DBG(" MainComponent::buttonClicked: loadButton");
FileChooser chooser{ "Select a file" };
if (chooser.browseForFileToOpen())
{
trackTitles.add(File{ chooser.getResult() });
player->loadURL(File{ chooser.getResult() });
}
}
}
do you need to add a button to the table? here's an example
.h
class PlaylistComponent : public juce::Component,
public TableListBoxModel,
public Button::Listener
{
.....
Component *refreshComponentForCell(int rowNumber, int columnId, bool , Component *existingComponentToUpdate) override;
void paintCell(Graphics &, int rowNumber, int columnId, int width, int height, bool ) override;
void buttonClicked(Button *button) override;
void setTracks(Array<File> tracksFile);
TableListBox tableComponent;
.....
}
.cpp
PlaylistComponent::PlaylistComponent()
{
// In your constructor, you should add any child components, and
// initialise any special settings that your component needs.
tableComponent.getHeader().addColumn("", 1, 20);
tableComponent.getHeader().addColumn("Track Title", 3, 500);
tableComponent.setModel(this);
addAndMakeVisible(tableComponent);
}
Component * PlaylistComponent::refreshComponentForCell(int rowNumber, int columnId, bool , Component *existingComponentToUpdate)
{
if (columnId == ) // The ID and Length columns do not have a custom component
{
jassert(existingComponentToUpdate == nullptr);
return nullptr;
}
if(columnId == 1)
{
// Creating component if does not existst yet
TextButton * btn = static_cast<TextButton *> (existingComponentToUpdate);
if (btn == 0)
{
btn = new TextButton { "Play" };
}
String id{ std::to_string(rowNumber) };
btn->setComponentID(id);
btn->addListener(this);
existingComponentToUpdate = btn;
}
return existingComponentToUpdate;
}
void PlaylistComponent::paintCell(Graphics &g, int rowNumber, int columnId, int width, int height, bool )
{
if (columnId == 2) {
g.drawText(trackTitles[rowNumber], 2, 0, width - 4, height, Justification::centredLeft, true);
}
}
void PlaylistComponent::setTracks(Array<File> tracksFile)
{
for (int i = 0; i < tracksFile.size(); i++)
{
trackTitles.push_back(tracksFile[i].getFileName());
}
tableComponent.updateContent();
}
void PlaylistComponent::buttonClicked(Button *button)
{
//Array<File> file;
player->loadURL(file[std::stoi(button->getComponentID().toStdString())]); // your player
player->start();
}
then near the table there should be a button for loading a lot of audio file, you can store it in a vector
Array<File> file;
..........
void loadAudioFiles()
{
juce::String filters = "*.mp3";
FileChooser chooser("Select an mp3 files..", File::getSpecialLocation(File::userHomeDirectory), filters);
if (chooser.browseForMultipleFilesToOpen())
{
file = chooser.getResults();
playlistComponent.setTracks(file );
}
}
For my current project I have several classes defining levels for the game. All of the levels are of the following form:
class Level1
{
public:
Level1(int initialFrame);
void update(int frameCount, sf::RenderWindow* renderWindow);
void draw(sf::renderWindow* renderWindow);
bool finished;
};
Note that none of the definitions for the functions update and draw are the same between levels.
In main my loop looks something like this:
int main()
{
sf::RenderWindow renderWindow();
frames = 0;
progress = 0;
Level1* level1 = new Level1(0);
Level2* level2 = 0;
Level3* level3 = 0;
while (true) // Not actually like this, just put it here to illustrate my point
{
renderWindow.clear()
if (progress == 0)
{
level1->update(frames, &renderWindow)
level1->draw(&renderWindow)
if (level1->finished)
{
delete level1;
progress++;
level2 = new level2(frames);
}
}
if (progress == 1)
{
level2->update(frames, &renderWindow)
level2->draw(&renderWindow)
if (level2->finished)
{
delete level2;
progress++;
level3 = new level3(frames);
}
}
if (progress == 2)
{
level3->update(frames, &renderWindow)
level3->draw(&renderWindow)
if (level3->finished)
{
delete level3;
progress++;
}
}
frames++;
renderWindow.display();
}
}
So I wanted to simplify this as I have the similar blocks of code running over and over. To solve this I created a template as such:
template <class T>
void loop(T level, int frameCount, sf::RenderWindow* renderWindow, int* progress)
{
level->update(frameCount, renderWindow);
level->draw(renderWindow);
if (level->finished)
{
delete level;
*progress++;
}
}
This partly works, but I'm stuck on how to, a) Initialise the next level when one is finished in the template, and b) how to implement the template function in my game loop without using a check for progress, and manually programming in each level like I had before. Ideally I'd want something like a list of levels in the order I want them to be played, and progress could index which one I'm using the loop on, but I'm not sure how to implement that.
You should make a abstract base class of level:
class Level
{
public:
Level(int initialFrame);
virtual ~Level();
virtual void update(int frameCount, sf::RenderWindow* renderWindow) = 0;
virtual void draw(sf::renderWindow* renderWindow) = 0;
bool finished;
};
and let all your levels inherit from this class:
class Level1 : public Level
{
public:
Level1(int initialFrame);
void update(int frameCount, sf::RenderWindow* renderWindow) override;
void draw(sf::renderWindow* renderWindow) override;
};
And then just swap between them depending on the current progress.
Level* currentLevel = new Level1(0);
...
while (true)
{
currentLevel->update(frames, &renderWindow)
currentLevel->draw(&renderWindow)
if (currentLevel->finished)
{
delete currentLevel;
progress++;
switch(progress)
{
case 1: currentLevel = new Level2(...); break;
case 2: currentLevel = new Level3(...); break;
case 3: currentLevel = new Level4(...); break;
...
}
}
So you save yourself some if-blocks.
You're overthinking this.
Define a Level interface which is implemented by all your levels
class Level {
public:
virtual void update(int frameCount, sf::RenderWindow* renderWindow) = 0;
virtual void draw(sf::renderWindow* renderWindow) = 0;
virtual bool finished() = 0;
};
Then simply create a queue of levels and pop from it
auto Levels = std::queue<std::unique_ptr<Level>>();
Levels.push(std::make_unique<Level1>());
Levels.push(std::make_unique<Level2>());
Levels.push(std::make_unique<Level3>());
std::unique_ptr<Level> currentLevel = Levels.front();
Levels.pop();
while(true){
if(currentLevel->finished()){
if(Levels.empty()){
break;
currentLevel = std::move(Levels.front())
Levels.pop();
}
currentLevel->update(frames, &renderWindow)
currentLevel->draw(&renderWindow);
frames++;
renderWindow->display();
}
If you absolutely need to pass in the frames at construction time, or you don't want to allocate the memory ahead of time, you could make a list of functionals instead, where the function returns a std::unique_ptr<Level>.
I am using the JUCE framework to create an audio plugin. I have created a Knob class which inherits from the Component class. My Knob class contains references to a Slider & a Label.
In my AudioProcessorEditor class, I initialize several Knob objects. However, none of the Components are visible at run-time. Am I doing something wrong or missing a step?
I have tried declaring Slider & Label objects directly inside of my AudioProcessorEditor class. When I do that, I am able to see the Slider & Label objects successfully at run-time. So I get the feeling that the issue involves my Knob class.
Knob.h
#pragma once
#include "../JuceLibraryCode/JuceHeader.h"
class Knob : public Component
{
public:
Knob(String, AudioProcessorEditor*);
~Knob();
void paint (Graphics&) override;
void resized() override;
String Name;
Label *KnobLabel;
Slider *KnobSlider;
AudioProcessorEditor *APE;
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Knob)
};
Knob.cpp
#include "Knob.h"
Knob::Knob(String name, AudioProcessorEditor *ape)
{
// In your constructor, you should add any child components, and
// initialise any special settings that your component needs.
this->Name = name;
APE = ape;
KnobLabel = new Label(name);
KnobLabel->setColour(0x1000281, Colours::antiquewhite);
KnobLabel->setAlwaysOnTop(true);
KnobLabel->setFont(10);
KnobSlider = new Slider();
KnobSlider->setAlwaysOnTop(true);
addAndMakeVisible(KnobLabel);
addAndMakeVisible(KnobSlider);
}
void Knob::paint (Graphics& g)
{
/* This demo code just fills the component's background and
draws some placeholder text to get you started.
You should replace everything in this method with your own
drawing code..
*/
g.setColour(Colours::white);
g.fillAll();
}
void Knob::resized()
{
// This method is where you should set the bounds of any child
// components that your component contains..
auto bounds = getLocalBounds();
KnobSlider->setBounds(bounds.removeFromTop(getHeight()*8));
KnobLabel->setBounds(bounds);
}
PluginEditor.h
#pragma once
#include "../JuceLibraryCode/JuceHeader.h"
#include "PluginProcessor.h"
#include "Knob.h"
#include "Model.h"
class MoonlightAudioProcessorEditor : public AudioProcessorEditor
{
public:
MoonlightAudioProcessorEditor (MoonlightAudioProcessor&);
~MoonlightAudioProcessorEditor();
void paint (Graphics&) override;
void resized() override;
Knob *OrbitKnob;
Knob *SpaceKnob;
Knob *InertiaKnob;
void ConfigureUI();
private:
OwnedArray<Knob> Knobs;
ComponentBoundsConstrainer ResizeBounds;
ResizableCornerComponent *Resizer;
MoonlightAudioProcessor& processor;
};
PluginEditor.cpp
#include "PluginProcessor.h"
#include "PluginEditor.h"
MoonlightAudioProcessorEditor::MoonlightAudioProcessorEditor (MoonlightAudioProcessor& p)
: AudioProcessorEditor (&p), processor (p)
{
setSize (400, 300);
ConfigureUI();
}
void MoonlightAudioProcessorEditor::ConfigureUI()
{
OrbitKnob = new Knob("Orbit", this);
SpaceKnob = new Knob("Space", this);
InertiaKnob = new Knob("Inertia", this);
Knobs.add(OrbitKnob);
Knobs.add(SpaceKnob);
Knobs.add(InertiaKnob);
for (Knob *knob : Knobs)
{
knob->KnobSlider->addListener(this);
addAndMakeVisible(knob);
knob->setAlwaysOnTop(true);
}
ResizeBounds.setSizeLimits(DEFAULT_WIDTH,
DEFAULT_HEIGHT,
MAX_WIDTH,
MAX_HEIGHT);
Resizer = new ResizableCornerComponent(this, &ResizeBounds);
addAndMakeVisible(Resizer);
setSize(processor._UIWidth, processor._UIHeight);
}
void MoonlightAudioProcessorEditor::paint (Graphics& g)
{
g.setColour (Colours::black);
g.fillAll();
}
void MoonlightAudioProcessorEditor::resized()
{
int width = getWidth();
int height = getHeight();
auto bounds = getLocalBounds();
auto graphicBounds = bounds.removeFromTop(height*.8);
auto orbitBounds = bounds.removeFromLeft(width/3);
auto spaceBounds = bounds.removeFromLeft(width/3);
if (OrbitKnob != nullptr)
{
OrbitKnob->setBounds(orbitBounds);
}
if (SpaceKnob != nullptr)
{
SpaceKnob->setBounds(spaceBounds);
}
if (InertiaKnob != nullptr)
{
InertiaKnob->setBounds(bounds);
}
if (Resizer != nullptr)
{
Resizer->setBounds(width - 16, height - 16, 16, 16);
}
processor._UIWidth = width;
processor._UIHeight = height;
}
Also, I have been using the AudioPluginHost application provided with JUCE to test my plugin. A lot of times the application will crash from a segmentation fault. Then I rebuild the plugin without changing anything and it will work.
I ended up figuring it out.
My components were being initialized after I set the size. A lot of the display logic was in the resize() function so I needed to set the size after component initialization.
You left out a call to Component::Paint(g) in Knob::Paint(Graphics& g)
I have lots of custom Qt widgets that have their own layouts and contain lists of even more widgets.
I ended up having a ManagerWidget that controls everything (including listening to all events) and propagating them to the widget that needs to update its content. It seems there are too many methods that just pass the information along.
Is there a better way of doing this? Connecting the classes through signal/slots doesn't really work in this case because there's a map of them and I wouldn't know which one was fired.
Would the Mediator pattern fit in this case?
class ManagerWidget : public QWidget {
public:
map<ID, MainWidgetContainer*> mainWidgetMap;
RecordsContainer *recentRecords; // This widget can contain any number of widgets, each showing a record
// events are caught here and propagated to the widgets
void AddNewEntry(ID id) {
MainWidgetContainer *w = new MainWidgetContainer(id);
mainWidgetMap.insert(id, w);
}
void UpdatePictureWidget(ID id) {
mainWidgetMap.value(id)->UpdatePictureWidget();
}
void AddXItem(ID id, Type1 k) {
if(id == 0)
recentRecords->AddXItem(k);
else
mainWidgetMap.value(id)->AddXItem(k);
}
void UpdateEntry(ID id, string newName) { }
};
class MainWidgetContainer : public QWidget {
public:
NameWidget *name;
PictureWidget *pic;
RecordsContainer *records; // This widget can contain any number of widgets, each showing a record
MainWidgetContainer(ID id) {
name = new NameWidget(id);
records = new RecordsContainer();
pic = new PictureWidget();
// setup layout and add items to records as they come in later
}
AddXItem(Type1 k) { records->AddXItem(k); }
UpdatePictureWidget() { pic->Update(); }
};
class RecordsContainer : public QWidget {
public:
map<Type1, Item*> itemMap;
AddXItem(Type1 k) {
Item *item = new Item(k);
itemMap.insert(k, item);
mainLayout->addWidget(item);
}
AddYItem(Type2 k);
// etc
};