I want to fit strings into a specific width. Example, "Hello world" -> "...world", "Hello...", "He...rld".
Do you know where I can find code for that? It's a neat trick, very useful for representing information, and I'd like to add it in my applications (of course).
Edit: Sorry, I forgot to mention the Font part. Not just for fixed width strings but according to the font face.
views:
413answers:
5It's really pretty trivial; I don't think you'll find specific code unless you have something more structured in mind.
You basically want:
- to get the length of the string you have, and the window width.
- figure out how many charaters you can take from the original string, which will basically be window width-3. Call that k.
- Depending on whether you want to put the ellipsis in the middle or at the right hand end, either take the first floor(k/2) characters from one end, concatenated with "...", then concatenated with the last floor(k/2) characters (with possibly one more character needed because of the division); or take the first k characters, ollowed by "...".
It's a pretty simple algorithm to write yourself if you can't find it anywhere - the pseudocode would be something like:
if theString.Length > desiredWidth:
theString = theString.Left(desiredWidth-3) + "...";
or if you want the ellipsis at the start of the string, that second line would be:
theString = "..." + theString.Right(desiredWidth-3);
or if you want it in the middle:
theString = theString.Left((desiredWidth-3)/2) + "..." + theString.Right((desiredWidth-3)/2 + ((desiredWidth-3) mod 2))
Edit:
I'll assume you're using MFC. Since you're wanting it with fonts, you could use the CDC::GetOutputTextExtent function. Try:
CString fullString
CSize size = pDC->GetOutputTextExtent(fullString);
bool isTooWide = size.cx > desiredWidth;
If that's too big, then you can then do a search to try and find the longest string you can fit; and it could be as clever a search as you want - for instance, you could just try "Hello Worl..." and then "Hello Wor..." and then "Hello Wo..."; removing one character until you find it fits. Alternatively, you could do a binary search - try "Hello Worl..." - if that doesn't work, then just use half the characters of the original text: "Hello..." - if that fits, try halfway between it and : "Hello Wo..." until you find the longest that does still fit. Or you could try some estimating heuristic (divide the total length by the desired length, proportionately estimate the required number of characters, and search from there.
The simple solution is something like:
unsigned int numberOfCharsToUse = fullString.GetLength();
bool isTooWide = true;
CString ellipsis = "...";
while (isTooWide)
{
numberOfCharsToUse--;
CString string = fullString.Left(numberOfCharsToUse) + ellipsis;
CSize size = pDC->GetOutputTextExtent(string);
isTooWide = size.cx > desiredWidth;
}
I think Smashery's answer is a good start. One way to get to the end result would be to write some test code with some test inputs and desired outputs. Once you have a good set of tests setup, you can implement your string manipulation code until you get all of your tests to pass.
- Calculate the width of the text ( based on the font)
In case you are using MFC the API GetOutputTextExtent will get you the value.
if the width exceeds the given specific width, calculate the ellipse width first:
ellipseWidth = calculate the width of (...)
Remove the string part with width ellipseWidth from the end and append ellipse.
something like: Hello...
For those who are interested for a complete routine, this is my answer :
/**
* Returns a string abbreviation
* example: "hello world" -> "...orld" or "hell..." or "he...rd" or "h...rld"
*
* style:
0: clip left
1: clip right
2: clip middle
3: pretty middle
*/
CString*
strabbr(
CDC* pdc,
const char* s,
const int area_width,
int style )
{
if ( !pdc || !s || !*s ) return new CString;
int len = strlen(s);
if ( pdc->GetTextExtent(s, len).cx <= area_width ) return new CString(s);
int dots_width = pdc->GetTextExtent("...", 3).cx;
if ( dots_width >= area_width ) return new CString;
// My algorithm uses 'left' and 'right' parts of the string, by turns.
int n = len;
int m = 1;
int n_width = 0;
int m_width = 0;
int tmpwidth;
// fromleft indicates where the clip is done so I can 'get' chars from the other part
bool fromleft = (style == 3 && n % 2 == 0)? false : (style > 0);
while ( TRUE ) {
if ( n_width + m_width + dots_width > area_width ) break;
if ( n <= m ) break; // keep my sanity check (WTF), it should never happen 'cause of the above line
// Here are extra 'swap turn' conditions
if ( style == 3 && (!(n & 1)) )
fromleft = (!fromleft);
else if ( style < 2 )
fromleft = (!fromleft); // (1)'disables' turn swapping for styles 0, 1
if ( fromleft ) {
pdc->GetCharWidth(*(s+n-1), *(s+n-1), &tmpwidth);
n_width += tmpwidth;
n--;
}
else {
pdc->GetCharWidth(*(s+m-1), *(s+m-1), &tmpwidth);
m_width += tmpwidth;
m++;
}
fromleft = (!fromleft); // (1)
}
if ( fromleft ) m--; else n++;
// Final steps
// 1. CString version
CString* abbr = new CString;
abbr->Format("%*.*s...%*.*s", m-1, m-1, s, len-n, len-n, s + n);
return abbr;
/* 2. char* version, if you dont want to use CString (efficiency), replace CString with char*,
new CString with _strdup("") and use this code for the final steps:
char* abbr = (char*)malloc(m + (len-n) + 3 +1);
strncpy(abbr, s, m-1);
strcpy(abbr + (m-1), "...");
strncpy(abbr+ (m-1) + 3, s + n, len-n);
abbr[(m-1) + (len-n) + 3] = 0;
return abbr;
*/
}