TcMenu supports a wide range of rendering devices, from HD44780 based units using our LiquidCrystal fork through to mono OLEDs and full colour TFT displays using Adafruit_GFX and TFT_eSPI library. Over to the left you see an example of rendering to OLED device with title widgets.
You can also easily take over the display to draw your own screen at any time. This is such a large subject, it deserves a page of its own.
How a menu will look on the device will largely depend on which display is used. However, there are a few common features of all displays. They can generally all have a title, and the title can nearly always contain title widgets. Title widgets provide a way to present the graphical state of something within the system in a small icon, the most common would be the signal strength indicator, or a connection status icon. An example showing this is presented below:
In order to allow for a wide range of displays, we provide multiple extension points within the rendering class hierarchy, and keep as much functionality as possible in the core.
From the above diagram we can see that most graphical and LCD displays (except Uno cases) extend from at least the BaseGraphicalRenderer
. And in fact all the true graphical displays extend from GraphicsDeviceRenderer
and then have a custom drawable. The benefit of GraphicsDeviceRenderer
is that does all the complex logic, and the drawable just has to implement the drawing glue code that calls into the library.
BaseMenuRenderer
in BaseRenderers.h - this just provides a few functions to help formatting items, taking over the display and handling dialogs.BaseGraphicalRenderer
in graphics/BaseGraphicalRenderer.h - extends from above, provides grid based capabilities, but does not do the drawing.GraphicsDeviceRenderer
in graphics/GraphicsDeviceRenderer.h - does all the drawing to an instance of Drawable
In all cases the display plugins will create a global variable called renderer
in your sketch. It will be at least of type MenuRenderer
meaning that you can rely on an absolute base set of functionality. In most cases it will be of BaseGraphicalRenderer
or GraphicsDeviceRenderer
so you will be able to rely on nearly all functions being available.
Usually, the renderer is initialised during menu setup and this starts a task manager timer task that calls the display back frequently to check if anything needs drawing. It is this task that keeps the screen up-to-date.
BaseDialog
in BaseDialog.h - for Uno low memory renderersMenuBasedDialog
in BaseDialog.h - for all other renderersIt is also possible to present a simple dialog for either information, or Yes / No question onto almost all supported displays. Along with this you can optionally decide to let the dialog show on any remote connections, taking over the top of the display similar to an Alert Box.
Before working with dialogs include the header:
#include <BaseDialog.h>
This includes the functions needed to work with dialogs.
Firstly, if we are interested in the outcome of the dialog, we must give the dialog a function to call us back upon when complete:
//
// this method is called when the dialog is dismissed.
//
void onDialogFinished(ButtonType btnPressed, void* /*userdata*/) {
if(btnPressed != BTNTYPE_OK) {
// do something if OK was pressed.
}
}
Next we get hold of the dialog and initialise it. Here we choose a dialog with OK and CANCEL as options:
BaseDialog* dlg = renderer.getDialog();
dlg->setButtons(BTNTYPE_OK, BTNTYPE_CANCEL, 1);
If you are using a menu based dialog (nearly all except Uno renderers), you can get hold of it directly without casting using:
withMenuDialogIfAvailable([](MenuBasedDialog* dlg) {
// code that relies on the menu based dialog.
}
Now we call the show method to make the dialog take over the screen
// for all cases and versions, you call show providing a character arry in progmem.
dlg->show(pgmHeaderText, remoteAllowed, onDialogFinished); // true = shows on remote sessions.
There are also newer versions of show that have additional features:
// additionally for newer versions you can provide header text that is located in RAM
void showRam(const char* headerRam, bool allowRemote, CompletedHandlerFn completedHandler = NULL);
Lastly we can copy some text into the second line of the dialog (not program memory):
dlg->copyIntoBuffer("Hello");
Controllers allow you far more control over a dialog, you can not only add additional menu items and buttons to the dialog, but you can also be informed when dialog buttons are pressed before the dialog ends, and be informed when it is initialising.
// and you can even use a controller, to provide additional functionality when buttons are
// pressed or change the text for buttons
void showController(bool allowRemote, BaseDialogController* controller);
Where the BaseDialogController
interface is implemented as follows:
class MyController : public BaseDialogController {
public:
void initialiseAndGetHeader(BaseDialog* dialog, char* buffer, size_t bufferSize) override {
// Here we should initialise the controller and fill in the buffer provided with the title
}
void dialogDismissed(ButtonType buttonType) override {
// this is called when the dialog is dismissed, the button that was clicked is provided.
}
bool dialogButtonPressed(int buttonNum) override {
// this is called when a dialog button is pressed, returning true will continue default processing,
// returning false prevents additional default processing
}
void copyCustomButtonText(int buttonNumber, char* buffer, size_t bufferSize) override {
// this will be used to get the text for a button, fill in the buffer with the text.
}
};
You can add additional menu items of any type to the dialog, you can even add more buttons, additional buttons should be of this type Dialog Button Menu Item.
void insertMenuItem(MenuItem* item);
Lastly we can copy some text into the second line of the dialog (not program memory):
dlg->copyIntoBuffer("Hello");
The easiest way to use touch support, is from tcMenuDesigner where it can be automatically added to appropriate display devices, this just explains how designer adds touch support for those who want more information, or wish to do it manually.
Touch screen support is added in two steps, first you must tell the renderer that you’re going to use a touch screen:
void setHasTouchInterface(bool hasTouch);
Then you must create a touch screen manager and encoder, here we show for resistive touch screen but there are many other options:
using namespace iotouch;
ResistiveTouchInterrogator touchInterrogator(xplus, xminus, yplus, yminus);
MenuTouchScreenManager touchScreen(touchInterrogator, renderer, rotation);
Then in your setup method:
touchScreen.start();
menuMgr.initWithoutInput(renderer, &rootMenuItem());
See the new guide to creating and using TitleWidgets and bitmaps.
You can draw to any of our displays using the same DeviceDrawable
, it is implemented everywhere we have a graphical plugin, the code remains the same in more or less all cases. All the drawing possibilities are documented, read the full device drawable documentation.
However, as a summary, we first get hold of the display drawable and tell the drawable we want to start using it:
auto drawable = renderer.getDeviceDrawable();
drawable->startDraw();
Next set the color that we want to use, there are two choices, set both the draw color and the background, or just change the draw color:
drawable->setColors(RGB(255, 128, 0), RGB(0,0,0));
drawable->setDrawColor(RGB(0,0,255));
We can get the display dimensions as a Coord
Coord size = drawable->getDisplayDimensions();
Now we can go ahead on draw on the device:
drawable->drawBox(Coord(0,0), Coord(320, 20), true);
drawable->drawCircle(Coord(50, 50), 10, true);
For drawing text can get the extents and draw as follows:
// measure the text we are going to put onscreen the coord value
// has the x and y dimensions. Baseline, the baseline offset.
int baseline = 0;
Coord coord = drawable->textExtents(myFont, 0, "Test", &baseline);
// drawing text, simply set the draw color and then text.
drawable->drawText(Coord(0,0), myFont, 0, "Test");
Drawing bitmaps is covered by two cases, with the DrawableIcon bitmap case being more configurable, while there is also support to draw an X-Bitmap (xbm) file too. Paint tool Gimp can create XBMs.
DrawableIcon icon(myIconId, Coord(width, height), DrawableIcon::ICON_XBITMAP, iconData);
drawable->drawBitmap(Coord(0,0), &drawableIcon, selected);
drawable->drawXBitmap(Coord(0,0), Coord(16, 16), xbmpData);
Lastly we stop drawing to ensure the screen is updated call
drawable->endDraw();
A couple of often used types:
Coord(x,y)
represents a coordinate in terms of x,y. There is a constructor that takes x and y point pair. It has members x, and y. It can safely be used by value.RGB(r, g, b)
represents a color in Red, Green and Blue, each value is between 0..255. Depending on the device driver, this can be assigned to color_t
which holds a color object.BLACK
and WHITE
are nearly always defined.Some displays are buffered by default, these include nearly all monochrome displays and the LTDC frame buffer support. However, TFT displays are typically not buffered into local memory as the memory requirements would be too high. However, if instead of buffering all 16-bit (or 32-bit) color information, we only buffer a palette then only 2 or 4 bits are needed per pixel. This reduces the memory requirement by about 4-8 times.
Further, we only tend to draw one thing at once, so if we also reduce the height of the buffer, for example, to handle the largest menu item height memory is reduced further. In the case of a 320x40 4 color palette buffer for Adafruit_GFX memory requirement is about 3200 bytes. For TFT_eSPI, the requirement would be about 6400 bytes as the buffers are 4 bit (16 color).
You can check if a device supports sub drawing as follows:
SubDeviceType subType = rootDrawable->getSubDeviceType();
Where the return will be one of NO_SUB_DEVICE
, SUB_DEVICE_4BPP
, SUB_DEVICE_2BPP
, and gives you an idea what type of buffer is available for use. If the sub-device type indicates that a sub device is available, you can then call:
DeviceDrawable* rootDrawable->getSubDeviceFor(Coord(x,y), Coord(w, h), palette, paletteSize);
CAUTION: It will return nullptr
if the dimensions are beyond the size that is supported. These dimensions are normally set in the code generator plugin as the line buffer size. If you’re using tcMenu code generator, then the lines to buffer are set up
If you’re using a sub device (IE effectively a memory buffer), and it is palette based, you can map a color_t
to a palette index:
color_t getUnderlyingColor(color_t col);
As we said earlier this is not the complete list, see the documentation or one of the examples for a more complete guide: