Reputation: 22327
My goal is to dynamically put some arbitrary text into an HICON image (at runtime.) I'm using the following code:
//Error checks are omitted for brevity
//First create font
LOGFONT lf = {0};
lf.lfHeight = -58;
lf.lfWeight = FW_NORMAL;
lf.lfOutPrecision = OUT_TT_PRECIS; //Use TrueType fonts for anti-alliasing
lf.lfQuality = CLEARTYPE_QUALITY;
lstrcpy(lf.lfFaceName, L"Segoe UI");
HFONT hFont = ::CreateFontIndirect(&lf);
//HICON hIcon = original icon to use as a source
//I'm using a large 256x256 pixel icon
hIcon = (HICON)::LoadImage(theApp.m_hInstance, MAKEINTRESOURCE(IDI_ICON_GREEN_DIAMOND), IMAGE_ICON, 256, 256, LR_DEFAULTCOLOR);
ICONINFO ii = {0};
::GetIconInfo(hIcon, &ii);
BITMAP bm = {0};
::GetObject(ii.hbmColor, sizeof(bm), &bm);
SIZE szBmp = {bm.bmWidth, bm.bmHeight};
HDC hDc = ::GetDC(hWnd);
HDC hMemDC = ::CreateCompatibleDC(hDc);
HGDIOBJ hOldBmp = ::SelectObject(hMemDC, ii.hbmColor);
HGDIOBJ hOldFont = ::SelectObject(hMemDC, hFont);
::SetBkMode(hMemDC, TRANSPARENT);
::SetTextColor(hMemDC, RGB(255, 0, 0)); //Red text
//Draw text
//NOTE that DrawText API behaves in a similar way
::TextOut(hMemDC, 0, 0, L"Hello", 5);
::SelectObject(hMemDC, hOldFont);
::SelectObject(hMemDC, hOldBmp);
//We need a simple mask bitmap for the icon
HBITMAP hBmpMsk = ::CreateBitmap(szBmp.cx, szBmp.cy, 1, 1, NULL);
ICONINFO ii2 = {0};
ii2.fIcon = TRUE;
ii2.hbmColor = ii.hbmColor;
ii2.hbmMask = hBmpMsk;
//Create updated icon
HICON hIcon2 = ::CreateIconIndirect(&ii2);
//Cleanup
::DeleteObject(hBmpMsk);
::DeleteDC(hMemDC);
::ReleaseDC(hWnd, hDc);
::DeleteObject(ii.hbmColor);
::DeleteObject(ii.hbmMask);
::DeleteObject(hFont);
and then I can display the icon in my window from OnPaint()
handler (so that I can see how it turns out) as such:
::DrawIconEx(dc.GetSafeHdc(), 0, 0,
hIcon2,
256, 256, NULL,
::GetSysColorBrush(COLOR_BTNFACE),
DI_NORMAL);
So here's what I get:
To see what's going on pixel-wise in my hIcon2
I called GetDIBits
on its ii.hbmColor
from the code above. The resulting pixel array where my word "Hello" was supposed to be shown looked like this:
The pixels are encoded as BGRA
in that memory dump, so the 4th byte in each DWORD stands for transparency: 0=transparent, FF=opaque. But in this case TextOut
doesn't fill out transparency, or leaves it as 0, which is interpreted as "fully transparent." Instead it seems to pre-multiply it into the RGB
colors themselves.
Note that if I keep looking further down the same bitmap, where the green diamond begins, the image pixels seem to have transparency bytes set correctly:
Any idea how to draw text so that the API could set those transparency bytes?
EDIT: As was suggested below I tried the following GDI+ method:
HGDIOBJ hOldBmp = ::SelectObject(hMemDC, ii.hbmColor);
Graphics grpx(hMemDC);
RectF rcfTxt(0.0f, 0.0f, (REAL)szBmp.cx, (REAL)szBmp.cy);
Font gdiFont(L"Segoe UI", 58.0f, FontStyleRegular, UnitPixel);
SolidBrush gdiBrush(Color(255, 0, 0));
StringFormat gdiSF;
gdiSF.SetAlignment(StringAlignmentNear);
gdiSF.SetFormatFlags(StringFormatFlagsNoWrap);
gdiSF.SetHotkeyPrefix(HotkeyPrefixNone);
//The reason I was using GDI was because I was setting
//spacing between letters using SetTextCharacterExtra()
//Unfortunately with GDI+ this does not work!
HDC hTmpDC = grpx.GetHDC();
::SetTextCharacterExtra(hTmpDC, -4); //This doesn't do anything!
grpx.ReleaseHDC(hTmpDC);
grpx.DrawString(L"Hello", 5, &gdiFont, rcfTxt, &gdiSF, &gdiBrush);
::SelectObject(hMemDC, hOldBmp);
and besides not being able to set character spacing (which I could with GDI using SetTextCharacterExtra
) here's what I got (slightly enlarged for visibility):
So clearly still an issue with transparency.
Upvotes: 3
Views: 4577
Reputation: 1
https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-antialiasing-with-text-use
Gdiplus::Bitmap bmp( your_Width, your_Height, PixelFormat64bppARGB);
//PixelFormat64bppARGB ARGB needed
FontFamily fontFamily(L"Arial");
Font font(&fontFamily, 29, FontStyleRegular, UnitPoint);
Gdiplus::RectF rectF(00.0f, 10.0f, your_Width, your_Height);
StringFormat stringFormat;
SolidBrush solidBrush(Color(63, 0, 0, 255));
stringFormat.SetAlignment(StringAlignmentCenter);
//solidBrush Color(63, 0, 0, 255) ARGB neede
graphics.SetTextRenderingHint(TextRenderingHintAntiAlias);
graphics.DrawString("your_text", -1, &font, rectF, &stringFormat, &solidBrush);
//TextRenderingHintAntiAlias this needed
Upvotes: 0
Reputation: 11
This reply is coming almost 3 years after the question was posted, but people still consult these things long into the future. So I'll explain what's happening.
DrawText (and other GDI text functions) will work on a transparent bitmap. The text is not coming out black even though it displays that way. The alpha channel is set to 0 on all pixels the text draws to, overriding whatever alpha you had set previously. If you set an alpha value in SetTextColor the text will render all black. If you're feeling ambitious you can run through pixel by pixel and target anything not your fill color (which requires a single fill color) but the problem then becomes one of the nature of ClearType being overridden and all alphas are set to whatever you set them to. The text ends up looking very funky. If you use a constant alpha for your background fill you can simply do a blanket run across the entire bitmap's bits after the text is drawn and reset all the alpha values. Since you have to read a byte to determine if it's background or not, you might as well just set every pixel's alpha to whatever the standard alpha is for that image and bypass the slow compares. This works reasonably well and I've found it to be very acceptable. In this day and age, MS should have taken care of this long ago but it's not to be.
Upvotes: 1
Reputation: 10916
Taken from an old post by Microsoft MVP Mike D Sutton here.
When you create a DC it initially has default 'stock' objects selected into it, including the stock 1*1*1 Bitmap. Since there is a Bitmap already selected into the DC when you call DrawText() it will still try and render to it even though pretty much everything (apart from one pixel) will be clipped.
What you need to do is to create a Bitmap, either DDB or DIBSection, and select that into your DC before drawing to it.
First though you need to find the size of your Bitmap since you want it large enough to display your text in, so for that you use the DrawText() call again on the initial DC but include the DT_CALCRECT flag. What this does is rather than drawing anything it simply measures how large the text is and dumps that into the RECT you pass the call. From here you can go ahead and create your DIBSection using those dimensions and select it into your DC. Finally perform your existing DrawText ()call (you may also want to use SetBkMode/Color()) which will render the text to the DIBSection from which you can get at the data.
This seems to work pretty well here:
HBITMAP CreateAlphaTextBitmap(LPCSTR inText, HFONT inFont, COLORREF inColour) { int TextLength = (int)strlen(inText); if (TextLength <= 0) return NULL; // Create DC and select font into it HDC hTextDC = CreateCompatibleDC(NULL); HFONT hOldFont = (HFONT)SelectObject(hTextDC, inFont); HBITMAP hMyDIB = NULL; // Get text area RECT TextArea = {0, 0, 0, 0}; DrawText(hTextDC, inText, TextLength, &TextArea, DT_CALCRECT); if ((TextArea.right > TextArea.left) && (TextArea.bottom > TextArea.top)) { BITMAPINFOHEADER BMIH; memset(&BMIH, 0x0, sizeof(BITMAPINFOHEADER)); void *pvBits = NULL; // Specify DIB setup BMIH.biSize = sizeof(BMIH); BMIH.biWidth = TextArea.right - TextArea.left; BMIH.biHeight = TextArea.bottom - TextArea.top; BMIH.biPlanes = 1; BMIH.biBitCount = 32; BMIH.biCompression = BI_RGB; // Create and select DIB into DC hMyDIB = CreateDIBSection(hTextDC, (LPBITMAPINFO)&BMIH, 0, (LPVOID*)&pvBits, NULL, 0); HBITMAP hOldBMP = (HBITMAP)SelectObject(hTextDC, hMyDIB); if (hOldBMP != NULL) { // Set up DC properties SetTextColor(hTextDC, 0x00FFFFFF); SetBkColor(hTextDC, 0x00000000); SetBkMode(hTextDC, OPAQUE); // Draw text to buffer DrawText(hTextDC, inText, TextLength, &TextArea, DT_NOCLIP); BYTE* DataPtr = (BYTE*)pvBits; BYTE FillR = GetRValue(inColour); BYTE FillG = GetGValue(inColour); BYTE FillB = GetBValue(inColour); BYTE ThisA; for (int LoopY = 0; LoopY < BMIH.biHeight; LoopY++) { for (int LoopX = 0; LoopX < BMIH.biWidth; LoopX++) { ThisA = *DataPtr; // Move alpha and pre-multiply with RGB *DataPtr++ = (FillB * ThisA) >> 8; *DataPtr++ = (FillG * ThisA) >> 8; *DataPtr++ = (FillR * ThisA) >> 8; *DataPtr++ = ThisA; // Set Alpha } } // De-select bitmap SelectObject(hTextDC, hOldBMP); } } // De-select font and destroy temp DC SelectObject(hTextDC, hOldFont); DeleteDC(hTextDC); // Return DIBSection return hMyDIB; }
If you need an example of how to call it then try something like this (inDC is the DC to render to):
void TestAlphaText(HDC inDC, int inX, int inY) { const char *DemoText = "Hello World!\0"; RECT TextArea = {0, 0, 0, 0}; HFONT TempFont = CreateFont(50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Arial\0"); HBITMAP MyBMP = CreateAlphaTextBitmap(DemoText, TempFont, 0xFF); DeleteObject(TempFont); if (MyBMP) { // Create temporary DC and select new Bitmap into it HDC hTempDC = CreateCompatibleDC(inDC); HBITMAP hOldBMP = (HBITMAP)SelectObject(hTempDC, MyBMP); if (hOldBMP) { BITMAP BMInf; // Get Bitmap image size GetObject(MyBMP, sizeof(BITMAP), &BMInf); // Fill blend function and blend new text to window BLENDFUNCTION bf; bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.SourceConstantAlpha = 0x80; bf.AlphaFormat = AC_SRC_ALPHA; AlphaBlend(inDC, inX, inY, BMInf.bmWidth, BMInf.bmHeight, hTempDC, 0, 0, BMInf.bmWidth, BMInf.bmHeight, bf); // Clean up SelectObject(hTempDC, hOldBMP); DeleteObject(MyBMP); DeleteDC(hTempDC); } } }
All credit to answer and code go to original posters on that forum, I've simply reposted it so that this answer will be valid if the links die.
Upvotes: 3