GUI that supports many TFT/GFX libraries

Status
Not open for further replies.

Projectitis

Well-known member
Hi all,

Bit of background - I'm a seasoned programmer (20+ years professional) but less c/c++ experience than I would like.

So I'm working on a GUI library that will support Adafruit_GFX, Paul's fast t3 library and Franks' DMA version, and also sumotoy's RA8875 library and others. The aim of the library is to provide very simple-to-use but beautiful GUI controls for your projects - you can spend less time thinking about your GUI and more on your project :)

Anyhow, the way this works is that you pass an instance of your library into the constructor like so:

Code:
#define TFT_DC  9
#define TFT_CS 10

ILI9341_t3 tft;
Projectitis_GUI gui;

void setup() {
  tft = new ILI9341_t3(TFT_CS, TFT_DC);
  gui = new Projectitis_GUI( tft );

Unfortunately, the various GFX libraries do not inherit from a common class that provide a common set of methods that my GUI library will need (e.g. drawRect, drawCircle, drawLine etc).

My method of dealing with that is creating a base class (GFXLibrary) like this:

Code:
class GFXLibrary{
	public:
		void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
		void drawCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color);
		void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
}

And then creating a subclass for each supported library, creating basically a pass-thru method for each supported method, like the following:

Code:
//GFXLibrary_ILI9431_t3.h
class GFXLibrary_ILI9431_t3 : public GFXLibrary{
	public:
		GFXLibrary_ILI9431_t3( ILI9341_t3 agfx);
		ILI9431_t3 *gfx;
		void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
		void drawCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color);
		void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
}

//GFXLibrary_ILI9431_t3.cpp

GFXLibrary_ILI9431_t3::GFXLibrary_ILI9431_t3( ILI9431_t3 agfx ){
	gfx = &agfx;
}
void GFXLibrary_ILI9431_t3::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color){
	gfx->drawRect( x,y, w,h, color );
}
void GFXLibrary_ILI9431_t3::drawCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color){
	gfx->drawCircle( x0,y0, r, color );
}
void GFXLibrary_ILI9431_t3::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color){
	gfx->drawCircle( x0,y0, x1,y1, color );
}

So my question is - is this the most (or at least "an") efficient way of achieving this in c++?
 
Inheritiance is a good way to solve this problem. Here's the way I might approach it, but it is certainly not the only way.

If there is any common implementation between them (say setting the SPI pins) then these should be implemented in the base class. Everything that is specific to each graphics library should be a pure virtual method. i.e. virtual methodName() = 0.

You then create concrete classes that inherit the base class and provide implementations for the virtual methods in the base class. These methods on the concrete classes will perform all the library specific stuff.

Code:
class GraphicsBase {
public:
    void setCS(int cs) { m_cs = cs; } // this is implemented in the base class because it's common
    virtual void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) = 0; // this must be implemented by the concrete class
private:
    int m_cs;
}

class GraphicsILI9431 : public GraphicsBase{
public:
    void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override {
        // ... specific code to draw a rectange via ILI9341 library...
    }
}

class GraphicsAdafruit : public GraphicsBase{
public:
    void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color)  override {
        // ... specific code to draw a rectange via Adafruit library
    }
}

In the code above, you cannot instantiate the GraphicsBase directly because it has at least one unimplemented member function (pure virtual). Users will instantiate the concrete class instead. If the user also wants to abstract away which particular concrete class they are using they would be able to treat an instance of GraphicsILI9341 or GraphicsAdafruit as GraphicsBase since they are guaranteed to have all the methods exposed by the base class.

If this looks like an approach that works for you, do some quick googling on the following c++ terms:
- pure virtual member functions
- upcasting (treating a child class as a base class)
- downcasting (treating a base class as a child class)
 
Hi Blackaddr, yes that is the approach that I'm suggesting above, but without the exact language to explain it - so thanks :) I was assuming downcasting is the default (as it is with most other languages) but am I wrong?

I was wondering about the impact of the overhead of wrapping each method call (the pass-through method GraphicsAdafruit::drawRect for example) and wondered if a different approach might be better.
The following approach uses callbacks (or function pointers, or whatever you call them in c++). I'm not sure if this is a better solution or not. Please excuse exact syntax, just focus on the technique:

Code:
typedef void(*FunctionWith5Uints)(int16_t, int16_t, int16_t, int16_t, int16_t);

class GraphicsBase {
public:
	FunctionWith5Uints drawRect;
}

class GraphicsILI9431 : public GraphicsBase{
public:
	GraphicsILI9431( ILI9431_t3 gfx ){
		drawRect = &gfx->drawRect;
	}
}

class GraphicsAdafruit : public GraphicsBase{
public:
    GraphicsAdafruit( Adafruit_GFX gfx ){
		drawRect = &gfx->drawRect;
	}
}

So GraphicsBase::drawRect is actually just a function pointer, and each subclass assigns the function pointers to the relevant methods of the chosen library. This assumes that each subclass has implemented the method with the same arguments in the same order, of course, which might be why this technique won't work.

But would it be more efficient/faster/less memory overhead this way?
 
it sounds like you're concerned about a member function on the derived class doing nothing but calling a function on the base class, resulting in two context switches on the stack.

If you write a function that calls a function, that's potentially what will happen. But for derived classes, it doesn't matter if you are calling a member function implemented on the base class, or one implemented on the derived class, there will be only one layer of function call.

In practical terms though, any difference in performance between using inheritance or function pointers is insignificant compared to the workload of the SPI processing when talking to the displays. Just choose which code style you like and feel more comfortable with. They are both equally valid in my opinion.
 
Ah, great, thanks Blackaddr. Normally I'd just use derived classes without a second thought as they're easier to read and understand, but since this is a microprocessor we're talking about, I didn't know what the implications were. All good.
 
FYI mt ili9341_t3n library has Franks Dma like stuff in it. I started by adding the code like his and then integrated more of the graphic primitives with the frame buffer code and also added better support for ding asych updates one shots. That is you do all of the graphic primitives and then say update now...
 
So, KurtE, is your library faster than t3 and t3DMA? And it has transaction support? Why are there so many versions :eek: and not just all implemented in the same library?
 
Status
Not open for further replies.
Back
Top