Passing negative int/long/float/double values to Print.h library

write needs to be implemented by you anyway so why not...

But why? Why just not inherit this method from the Print instead of coding it again?
I still sure that you dont understand how the Print class works.

What do you need to do to print on your LCD/OLED/etc - implement a write(uint8_t ) method only/. All other methods has been implemented already in Print class

how does your class knows the screen layout and what part of the screen needs a refresh if you print something new ? do you plan to provide some sort of area / zone definition first and then use the current cursor position to determine in which area you are?

this feels weird...

I would rather define a class that has a boundary rectangle and when you try to print that element it erases the bounding box and then actually print the data

2 Likes

I'm glad @J-M-L came up with a similar proposal.

This is an prototype I have made some days ago for another thread:

basically you have different types of elements (areas) on the display

```
Element groupH { 0, 42};
Element groupK {40, 42};
Element groupJ { 0, 42 + 13};
Element groupL {40, 42 + 13};
//
Element button {85, 42};
```

and then you just "print" to these elements

groupJ.trigger(tasterTXT[rnd]);

the element "knows" what to delete, and where to print.

Well I'm keeping specific info about that TextAnchor, the X/Y location of the cursor, its current value, and the font size. So with that, it's pretty easy to know exactly where to apply an update (example).

Odd, I don't see how its that much different than what you described, honestly.

TextAnchor * TextAnchor::print( void ){
  int diff = 0;

  // If the printed text isn't the same as the text printed last time, then check the difference
  // in length (to override the text that would otherwise show from the side), and update the 
  // _printerValPrev
  if ( strcmp( _printerVal, _printerValPrev ) != 0 ) {
    if ( strlen(_printerValPrev) > strlen( _printerVal ) ){
      diff = strlen(_printerValPrev) - strlen(_printerVal );
    }
  }

  GFXHandler->setTextSize( _textSize );
  GFXHandler->setFont( _textFont ); 
  GFXHandler->setCursor( _x, _y );
  GFXHandler->setTextColor( colFg, colBg );
  GFXHandler->print( _printerVal );

  for ( int i = 0; diff > i; i++ ){
    GFXHandler->print(" ");
  }

  // If the printed text isn't the same as the text printed last time, then check the difference
  // in length (to override the text that would otherwise show from the side), and update the 
  // _printerValPrev
  if ( strcmp( _printerVal, _printerValPrev ) != 0 ) {
    _lastChangeMs = millis();

    strcpy(_printerValPrev, _printerVal);
  }

  return this;
}

I do store the x/y location of the cursor, but instead of keeping the H/W of the box, I store data for the text (which would need to be stored anyways for when/if it needs to get updated), and just write over it like that.

I suppose I could just use the getTextBounds() method in the AdafruitGFX library, which would do the same thing as you described.

Yeah, that's pretty much exactly what I'm doing.. lol. I do store the X/Y locations of the text, I just don't store the H/W of it (as a "box"), but I store the value, font and font size, which makes writing over it just that much faster.

I don't see how drawing a box over it to hide the old text, then writing the text could be faster/better than just writing the old text over it (with the correct background color).
If you look at the example I shared in the reply just before this, youll see that all I do is write over it, then I execute

 for ( int i = 0; diff > i; i++ ){
    GFXHandler->print(" ");
  }

(diff = the difference in length between the old font and the new font). This seems to work pretty well, and its rather quick too.

Whats the best way to add a new character (the single uint8_t passed to write()) to a character array buffer?
In my previous version of this class, I was creating a new temporary char array, then using strcpy and strcat to add the buffer and new value to it, but I can't get that to work.

Here's the MyPrint.h file:

class MyPrint : public Print {
public: 
  MyPrint();
  ~MyPrint(){};

  size_t write(uint8_t);

  void flush( void );
private:
  // Current char/string being displayed (or to be displayed)
  unsigned char _printerVal[ MAX_CHAR_LENGTH ];  // MAX_CHAR_LENGTH = 60
};

This is the relevant method in the cpp file:

size_t MyPrint::write(uint8_t val){
  // _printerVal is the (private) buffer for this instance of MyPrint

  char newChar = (char) val;

  char newCharArr[ strlen(_printerVal) + 2 ]; // Length of _printerVal buffer, + 1 new char, + null term
  //char newCharArr[ MAX_CHAR_LENGTH ];

  strcpy( newCharArr, _printerVal ); // Add current _printerVal to temp char array...
  //strncpy( newCharArr, _printerVal, MAX_CHAR_LENGTH );

  strcat( newCharArr, newChar );
  //strcat( newCharArr, (char) val ); // Add the new character to the buffer

  strcpy(_printerVal, newCharArr);

  //delete [] newCharArr;

  std::cout << "Adding new character " << newChar << " to the buffer => _printerVal: " << _printerVal << std::endl;

  return sizeof(val);
}

The sketch im using to test this out:

#include "MyPrint.h"
#include <ArduinoSTL.h>

MyPrint * MP;

void setup() {
  Serial.begin(9600);
  delay(2000);

  MP = new MyPrint();

  MP->print("Hi");
}

void loop(){}

I'm expecting the output to look like

Adding new character H to the buffer => _printerVal: H
Adding new character i to the buffer => _printerVal: Hi

But the _printerVal ends up being empty:

Adding new character H to the buffer => _printerVal: 
Adding new character i to the buffer => _printerVal: 

Oh yeah, that makes more sense, lol. Thanks man

Thanks for the help everyone. I ended up just creating a 2nd Buffer class that extended the Print class, and the TextArea class then creates a new Buffer instance to manage the print value. And it works great.

TextBuffer.h

#pragma once

#ifndef __TEXT_BUFFER_H__
#define __TEXT_BUFFER_H__

#include <Arduino.h>
#include <Print.h>
#include <ArduinoSTL.h>
#include "Settings.h"


class TextBuffer : public Print {
public: 
  TextBuffer();
  ~TextBuffer(){};

  inline void clear(){ _bufferValue[ 0 ] = 0; }; 
  size_t write(uint8_t); 
  char * getValue();
 
  template <typename t> t setValue(t val){
    _bufferValue[ 0 ] = 0;
    this->print(val);
  }

private:
  char _bufferValue[MAX_CHAR_LENGTH] {'\0'};
};

#endif;

TextBuffer.cpp

#pragma once

#include "TextBuffer.h"

TextBuffer::TextBuffer() : Print () {}

size_t TextBuffer::write(uint8_t val){
  int currentLength = strlen( _bufferValue );
  _bufferValue[ currentLength ] = val;   
  _bufferValue[ currentLength + 1 ] = 0;
  return sizeof(val);
}

char * TextBuffer::getValue(){
  return _bufferValue;
}

TextArea.h

#pragma once

#ifndef __TEXTAREA_H__
#define __TEXTAREA_H__

#include <Arduino.h>
#include "TextBuffer.h"
#include <ArduinoSTL.h>
#include "Settings.h"

class TextArea {
public: 
  TextArea();
  inline TextArea(TextBuffer &_buffer) : _buffer (&_buffer) {}

  ~TextArea(){};

  template <typename t> t print(t val){    
    _buffer->clear();
    _buffer->setValue(val);
    strcpy( _printerVal, _buffer->getValue() );
    updateDisplay();
  }

  void updateDisplay( void );

private:
  char _printerVal[MAX_CHAR_LENGTH];

  TextBuffer * _buffer;
};

#endif

TextArea.cpp

#pragma once

#include "TextArea.h"

TextArea::TextArea()  {
  TextBuffer * bf = new TextBuffer();

  this->_buffer = bf;
}

void TextArea::updateDisplay( void ){
  // 
  // Logic for pushing the updated text to the display will go here..
  //
  std::cout << _printerVal;
}

Test.ino

#include "TextArea.h"
#include <ArduinoSTL.h>


TextArea * txtA;

void testLine( char * varType,  char * value ){
  char buf[41];
  sprintf( buf, "\n%-7s|%-15s => ", varType, value);
  std::cout << buf;
}


void setup() {
  Serial.begin(9600);
  delay(2000);

  txtA = new TextArea();
  
  char buf[41];

  sprintf( buf, "%-7s|%-15s => %s", "TYPE", "VALUE", "RESULT");
  std::cout << buf;

  testLine("String", "Hello");
  txtA->print(String("Hello"));

  testLine("char[]", "Hello World");
  txtA->print("Hello World");

  testLine("int", "123" );
  txtA->print(123);

  testLine("int8_t", "-123" );
  txtA->print((int8_t)-123);

  testLine("uint8_t", "255" );
  txtA->print((uint8_t)255);
  
  testLine("ulong", "millis()");
  txtA->print((unsigned long)millis());

  testLine("long", "millis()");
  txtA->print((long)-1231234);
  
  testLine("float", "3.14159" );
  txtA->print((float)3.14159);

  testLine("float", "-3.14159" );
  txtA->print((float)-3.14159);
  
  testLine("double", "3.14159" );
  txtA->print((double)3.14159);

  testLine("char", "3.14159" );
  txtA->print("3.14159");
}

void loop() {}

Seems to work pretty well:

There's a few kinks to work out, but I think it'll work :slight_smile:

Im sure encapsulating the buffer logic in a separate class seems odd, but it makes using it from the main TextArea class easier for a few reasons (eg: I wanted to use print() and the Print class already uses that method, so I would have to name it something else).

What I don’t get is how do you know the extent of the former bounding box and how you manage potential overlaps

For scrolling text sure there is no issue but if you have a fixed width and try to display something too long you’ll go in the next field

I don’t see how you work around that constraint if you don’t pass a width and height for the target text area. But may be that’s not an issue for your needs

Well in theory I could use the getTextBounds() method (or whatever it's called) and check for overlaps that way, but right now I'm not really accounting for overlaps. I guess it just hasn't really been an issue (at least not one I've ran into yet).

How would you account for overlaps with the x/y/w/h? Any time a new textarea instance is created (or moved), look through all of the existing textarea instances and see if there's already one that has a boundary that would intersect?

typically you design your screen layout with pre-defined areas for your items.

When they overlap it's likely that the two items won't be displayed at the same time (you can activate or deactivate an item)

When you want to display something in a text area you can constrain the text in the area and properly erase the previous content

1 Like

I see.
Also, I've just been testing with simple phrases/strings, but if I were to create a textarea with more content (ie: something that would result in multiple lines), I could see how restricting it to a box would help. Currently it would just run off the side of the screen (or return onto the next line on the other side of the screen).

Doesn't this give you a compiler error, or at least a warning? You promised the compiler that the 'print()' function would return a value of type 't'. But then you broke your promise by returning nothing.

Surprisingly it didn't, lol. But I noticed that after sharing it above, it's been fixed below (along with the append/prepend functions)

  // Printing a value - This clears the existing value in the buffer then
  // repopulates the buffer with the value provided.
  template <typename t> 
  TextArea * print( t val )
  {
    _buffer->clear();
    _buffer->setValue(val);

    return updateDisplay();

    return this;
  }

  // Appending text to existing buffer - This is done by just using the 
  // Print::print method without clearning the existing buffer. It will
  // just add the new string onto the end.
  template <typename t> 
  TextArea * append( t suffix )
  {    
    _buffer->print(suffix);

    return updateDisplay();

    return this;
  }

  // Add text to the beginning of a string - I couldn't think of a good way to 
  // just insert new character(s) in the beginning of the buffer using the
  // Print library, so this is kind of a work-around. It's not very clean, but
  // it works..
  template <typename t> 
  TextArea * prepend( t prefix )
  {    
    char currentBuffer[ sizeof(_buffer->getValue()) + 1 ];

    strcpy( currentBuffer, _buffer->getValue() );

    _buffer->clear();
    _buffer->print( prefix );
    _buffer->print( currentBuffer );

    return updateDisplay();

    return this;
  }

The print and append functions work fine, the prepend function is a little... messy. But it does work.

How i do my UI
I have a base class, UiControl
Then different UiElements inherit that class. UiLabel, UiDatagrid, UiButton

The label class will store the text in a char array, as well as some location information

I would then create a screen class which has a container of uiElements with update and draw - for clicking etc

When drawing the label it will check if the original value has changed, if it has, itterate through the characters that have changed, undraw the ones that have and redraw them.

Each uiElement also has an undraw method. When the active screen changes undraw is called on each element, so your not simply drawing a complete rectangle over the screen.

That sounds kinda similar to how I'm doing it.

The code you see above is actually just a junk sketch I was using to see if I could easily integrated the Print class. The actual library I'm working on has a UI class (here) which basically is just a simple factory for the other class that actually does most of the work (here).
The UI instance is created with some simple default values (the graphics handler, fg color, bg color, etc) which will get handed to the element class whenever you create a new instance.

The print method in the element class is what handles making sure the re-write works as expected (changing to the correct colors, font, size, location, etc), and deals with the previous text that may need to be hidden.
It also handles left/right alignment, which is pretty handy.

Im sure youll find some (or a lot) of the code to be somewhat subpar, lol. But this whole thing was more of an exercise to learn C++ and Arduino a bit more, but It's actually pretty handy. I def should redo it to incorporate more of what I've learned lately.

One thing I need to do is make it more compatible with other drivers so it can work with more than just the Adafruit_SSD1351 driver. I do have a couple other versions for other screens, but I need to find a better way of making it more cross-compatible.

Here's the example sketch I have for whats in the Github repo (in case you didn't want to open it)

#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>
#include <SPI.h>
#include "TextAnchor.hpp"
#include "SimpleUI.hpp"
#include "ColorCodes.hpp"

// START ROTARY ENCODER STUFF
#define ENCODER_DT      3     // Encoder output A (... or is it B?... idk)
#define ENCODER_CLK     4     // Encoder output B?
#define ENCODER_SW      2     // Encoder switch/button, will set the digipot to encoders val

int buttonState;                        // the current reading from the input pin
int lastButtonState             = LOW;  // the previous reading from the input pin

unsigned long lastDebounceTime  = 0;    // the last time the output pin was toggled
unsigned long debounceDelay     = 50;   // the debounce time; increase if the output flickers

volatile int encoderPos         = 0;    // Position of encoder, restricted to the ENCODER_[MAX/MIN] vals
volatile int currentStateCLK; // Input from encoders CLK pin
volatile int lastStateCLK;    // Value of the CLK in the last loop (checking for updates)
volatile bool clkClockwise;   // True if rotary is turning clockwise (this isn't really used).
// END ROTARY ENCODER STUFF


// declare size of working string buffers. Basic strlen("d hh:mm:ss") = 10
const size_t    MaxString               = 20;

// the string being displayed on the SSD1331 (initially empty)
char oldTimeString[MaxString]           = { 0 };

//Adafruit_SSD1351 oled = Adafruit_SSD1351(128, 128, &SPI, 10, 9, -1 );
Adafruit_SSD1351 oled(128, 128, &SPI, 10, 9, -1 );
//OledText Greeting(oled);

int32_t iter = 0;

uint8_t maxRows = 10;
int thisRow = 1;
char buffer[8];

SimpleUI SUI(oled);

// Display stuff
TextAnchor * TimeDisplay;

TextAnchor * Milliseconds;
TextAnchor * MillisecondsLabel;
TextAnchor * Minutes;
TextAnchor * MinutesLabel;
TextAnchor * Iterations;
TextAnchor * IterationsLabel;
TextAnchor * EncoderPos;
TextAnchor * EncoderPosLabel;

void setup() {
  Serial.begin(115200);

  delay(1000);

  pinMode(ENCODER_CLK,INPUT);
  pinMode(ENCODER_DT,INPUT);
  pinMode(ENCODER_SW, INPUT);

  // Read the initial state of CLK
  lastStateCLK = digitalRead(ENCODER_CLK);

  // Call updateEncoder() when any high/low changed seen
  // on interrupt 0 (pin 2), or interrupt 1 (pin 3)
  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), updateEncoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENCODER_DT), updateEncoder, CHANGE);
 
  SUI.begin(0x95e7, 0x0000);

  delay(1000);
  oled.fillRect(0, 0, 128, 128, BLACK); 
  delay(500);

  TimeDisplay   = SUI
    .newAnchor(20, 0)
    //.newAnchor(120, 0)->rightAlign(true) // Right align
    //->setColor(0xc7a9)
    ->print("<TimeDisplay>");
  //PumpDurtion   = SUI.newAnchor(0, 10)->setColor(LIGHTSTEELBLUE)->print("<PumpDurtion>");
  //PumpDrain     = SUI.newAnchor(0, 20)->setColor(LIGHTSTEELBLUE)->print("<PumpDrain>");
  //DrainDuration = SUI.newAnchor(0, 30)->setColor(LIGHTSTEELBLUE)->print("<DrainDuration>");

 
  MillisecondsLabel   = SUI.newAnchor(0, 15)
    //.newAnchor(65, 15)->rightAlign(true)
    ->setColor(DIMGRAY)->print("MS:");
  Milliseconds        = SUI.newAnchor(67, 15)->setColor(0xF800)->print("<ms>");

  MinutesLabel        = SUI.newAnchor(0, 25)
    //.newAnchor(65, 25)->rightAlign(true)
    ->setColor(DIMGRAY)->print("Min:");
  Minutes             = SUI.newAnchor(67, 25)->setColor(0x3666)->print("<min>");

  IterationsLabel     = SUI.newAnchor(0, 35)
    //.newAnchor(65, 35)->rightAlign(true)
    ->setColor(DIMGRAY)->print("Iterations:");
  Iterations          = SUI.newAnchor(67, 35)->setColor(0x4416)->print("<iter>");

  EncoderPosLabel     = SUI.newAnchor(0, 45)
    //.newAnchor(65, 45)->rightAlign(true)
    ->setColor(DIMGRAY)->setHighlightColor(MEDIUMSLATEBLUE)->print("Encoder:");
  EncoderPos          = SUI.newAnchor(67, 45)->setColor(DARKORANGE)
    //->setHighlightColor(ORANGERED)
    ->print("<enco>");

  delay(300);
  
}


void loop(){
  iter++;
  
  if ( millis() - TimeDisplay->lastChangeMs() > 1000 ){
    setUptime();
  }
  
  if ( millis() - Milliseconds->lastChangeMs() > 500 ){
    Milliseconds->print(millis());
  }

  if ( millis() - Minutes->lastChangeMs() > 60000 ){
    Minutes->print(millis()/60000);
  }

  if ( millis() - Iterations->lastChangeMs() > 250 ){
    Iterations->print(iter);
  }
  
  int reading = digitalRead(ENCODER_SW);

  // If the switch changed, due to noise or pressing:
  if (reading != lastButtonState) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    // if the button state has changed:
    if (reading != buttonState) {
      buttonState = reading;

      // only toggle the LED if the new button state is HIGH
      if (buttonState == LOW) {
        EncoderPos->highlight();
      }
    }
  }

  // set the LED:

  // save the reading. Next time through the loop, it'll be the lastButtonState:
  lastButtonState = reading;
}

void setUptime() {

    unsigned long upSeconds = millis() / 1000;
    unsigned long days = upSeconds / 86400;
    upSeconds = upSeconds % 86400;
    unsigned long hours = upSeconds / 3600;
    upSeconds = upSeconds % 3600;
    unsigned long minutes = upSeconds / 60;
    upSeconds = upSeconds % 60;

    char newTimeString[ MaxString ] = { 0 };

    // construct the string representation
    sprintf(
        newTimeString,
        "%lu %02lu:%02lu:%02lu",
        days, hours, minutes, upSeconds
    );

    // has the time string changed since the last oled update?
    if ( strcmp( newTimeString, oldTimeString ) != 0) 
        strcpy( oldTimeString, newTimeString );
 
    TimeDisplay->print(newTimeString);    
}

void updateEncoder(){


  // Read the current state of CLK
  currentStateCLK = digitalRead(ENCODER_CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1){

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(ENCODER_DT) != currentStateCLK) {

      encoderPos --;

      clkClockwise = false;//  ="CCW";
    } 
    else {
      // Encoder is rotating CW so increment
      encoderPos ++;

      clkClockwise = true;
    }

     EncoderPos->print(encoderPos);
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}

@J-M-L , quick follow up question about your method - How do you determine if the text is overflowing the bounds? Just use the Adafruit_GFX::getTextBounds method? (or something similar if you're using a different library)

And do you know of a way to determine the best point to inject some line returns to keep something in a square? Like if I want to constrain a text to a 120x120px box, but send a large string to it, I'm not sure how I could determine when to have the text go to a new line.
I suppose I could do something like store the pixel width of the character width for that particular font and calculate it that way, but that has a lot of problems.

Edit: I just found the setTextWrap method, I suppose I could just use that, lol. Or look at the logic it uses.
Still curious how you do it though.

Yes basically something like that

It’s quite complicated if you want to do it right and it depends if you allow to split a word in two (there are rules depending on language on where you can cut) or if you can inject a return only where spaces are. Text processors will also finely play with character spacing/kerning and width of the space between the words when you want to justify your text (align on both sides).t
The algorithm always start by parsing the text and determining segments that need to stays together (either a word or part of a word) and calculating the width of those. Then there is an iterative process where you see how many segments you can add to the line until it’s full - playing for example with spaces between words to fine tune the look (you distribute the spaces as evenly as possible between words on a line or adjust spaces to minimize large gaps within a line, which can result in a more aesthetically pleasing lay) and then go to next line. The last line is usually not justified in text processors.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.