If 48x48 or 64x64 icons are present in the Vista Shell how can you get the handle to display one in a TImage using SHGetFileInfo?
I'd like to select an icon from a imagelist that represents a folder path and display a 48x48 or 64x64 icon in a Timage.
// load the large system image for the current path into Image1
SHGetFileInfo( PChar( CurrentPath ), FILE_ATTRIBUTE_NORMAL, SFI,
SizeOf( TSHFileInfo ), SHGFI_ICON or SHGFI_LARGEICON or SHGFI_SHELLICONSIZE or
SHGFI_SYSICONINDEX or SHGFI_TYPENAME or SHGFI_DISPLAYNAME );
AImageIndex := SFI.iIcon;
ImageList2.GetBitmap( AImageIndex, Image1.Picture.Bitmap );
Bill
You must use the SHGetImageList function, to get the image list with the larger icons.
Here you have an example in delphi
uses ShellApi, Commctrl, ShlObj;
const
SHIL_LARGE = $00; //The image size is normally 32x32 pixels. However, if the Use large icons option is selected from the Effects section of the Appearance tab in Display Properties, the image is 48x48 pixels.
SHIL_SMALL = $01; //These images are the Shell standard small icon size of 16x16, but the size can be customized by the user.
SHIL_EXTRALARGE= $02; //These images are the Shell standard extra-large icon size. This is typically 48x48, but the size can be customized by the user.
SHIL_SYSSMALL = $03; //These images are the size specified by GetSystemMetrics called with SM_CXSMICON and GetSystemMetrics called with SM_CYSMICON.
SHIL_JUMBO = $04; //Windows Vista and later. The image is normally 256x256 pixels.
IID_IImageList: TGUID= '{46EB5926-582E-4017-9FDF-E8998DAA0950}';
function GetImageListSH(SHIL_FLAG:Cardinal): HIMAGELIST;
type
_SHGetImageList = function (iImageList: integer; const riid: TGUID; var ppv: Pointer): hResult; stdcall;
var
Handle : THandle;
SHGetImageList: _SHGetImageList;
begin
Result:= 0;
Handle:= LoadLibrary('Shell32.dll');
if Handle<> S_OK then
try
SHGetImageList:= GetProcAddress(Handle, PChar(727));
if Assigned(SHGetImageList) and (Win32Platform = VER_PLATFORM_WIN32_NT) then
SHGetImageList(SHIL_FLAG, IID_IImageList, Pointer(Result));
finally
FreeLibrary(Handle);
end;
end;
Procedure GetIconFromFile(aFile:String; var aIcon : TIcon;SHIL_FLAG:Cardinal);
var
aImgList : HIMAGELIST;
SFI : TSHFileInfo;
Begin
//Get the index of the imagelist
SHGetFileInfo(PChar(aFile), FILE_ATTRIBUTE_NORMAL, SFI,
SizeOf( TSHFileInfo ), SHGFI_ICON or SHGFI_LARGEICON or SHGFI_SHELLICONSIZE or
SHGFI_SYSICONINDEX or SHGFI_TYPENAME or SHGFI_DISPLAYNAME );
if not Assigned(aIcon) then
aIcon:= TIcon.Create;
//get the imagelist
aImgList:= GetImageListSH(SHIL_FLAG);
//extract the icon handle
aIcon.Handle:= ImageList_GetIcon(aImgList, Pred(ImageList_GetImageCount(aImgList)), ILD_NORMAL);
End;
You can use these functions in this way
var
hicon :TIcon;
begin
hicon:= TIcon.Create;
try
GetIconFromFile('C:\Tools\reflector\readme.htm',hicon,SHIL_JUMBO);
Image1.Picture.Icon.Assign(hIcon); //assign to timage
finally
hIcon.Free;
end;
end;
Read here: (Code in C++)
Getting the 16×16 and 32×32 icons on
Windows is relatively easy and is
often as simple as one call to
ExtractIconEx.
However, getting the extra large
(48×48) and jumbo (256×256) icons
introduced respectively by XP and
Vista is slighly more complex. This is
normally done by:
Getting the file information, in particular the icon index, for the
given file using SHGetFileInfo
Retrieving the system image list where all the icons are stored
Casting the image list to an IImageList interface and getting the
icon from there
We found that the index of the file was not correct because the incorrect icon was shown during testing of the code posted by RRUZ. The GetIconFromFile method was setting the index baised on the image count. We changed GetIconFromFile to use the SFI index ( aIndex := SFI.iIcon ) and the correct icon was obtained. Apparently the shellimagelist is constantly changing so the index was incorrect.
Thanks to all to assisted. This seems like a very good piece of code now.
procedure GetIconFromFile( aFile: string; var aIcon: TIcon;SHIL_FLAG: Cardinal );
var
aImgList: HIMAGELIST;
SFI: TSHFileInfo;
aIndex: integer;
begin // Get the index of the imagelist
SHGetFileInfo( PChar( aFile ), FILE_ATTRIBUTE_NORMAL, SFI, SizeOf( TSHFileInfo ),
SHGFI_ICON or SHGFI_LARGEICON or SHGFI_SHELLICONSIZE or SHGFI_SYSICONINDEX or SHGFI_TYPENAME or SHGFI_DISPLAYNAME );
if not Assigned( aIcon ) then
aIcon := TIcon.Create;
// get the imagelist
aImgList := GetImageListSH( SHIL_FLAG );
// get index
//aIndex := Pred( ImageList_GetImageCount( aImgList ) );
aIndex := SFI.iIcon;
// extract the icon handle
aIcon.Handle := ImageList_GetIcon( aImgList, aIndex, ILD_NORMAL );
end;
kicon
i use the appropriate kicon method (LoadFromFile/LoadFromModule/LoadFromModuleByIndex) depending on the source file type.
if these methods fail, i use PrivateExtractIconsA:
function PrivateExtractIcons(lpszFile: PChar; nIconIndex, cxIcon, cyIcon: integer; phicon: PHandle; piconid: PDWORD; nIcons, flags: DWORD): DWORD; stdcall; external 'user32.dll' name 'PrivateExtractIconsA';
and pass the resulting handle to kicon's LoadFromHandle method.
once it's been loaded into kicon, iterate over the icondata[] array to pick out the size you want. kicon has methods to convert the returned image to PNG.
Related
Windows 10 / C++ / Win32
I need to make a list of 'installed' Keyboards.
The components of my 'list' needs to contain:
The HKL for the keyboard.
The KLID for the keyboard.
The values of the registry values obtained from:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts
Example: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\00000409
Value: Layout File
Value: Layout Text
The only way I am aware of enumerating Keyboards is via GetKeyboardLayoutList(), which returns a list of HKL's.
Methodology which works for 'standard' keyboards (HKL's 04090409, 04070407,..).
TCHAR Buffer[50];
TCHAR Buffer2[50];
HKL LoadedKeyboardLayout;
// Hkl: 04090409
_stprintf_s(Buffer, (sizeof(Buffer)/sizeof(TCHAR)), _T("%08X"), (((UINT)Hkl >> 16) & 0xFFFF));
// Buffer: "0000409"
LoadedKeyboardLayout = LoadKeyboardLayout(Buffer, (UINT)0);
// It Loads
ActivateKeyboardLayout(LoadedKeyboardLayout, KLF_SETFORPROCESS);
// It Activates
GetKeyboardLayoutName(Buffer2);
// Buffer2: "00000409"
// I can now fish the registry HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\00000409
This DOES not work when the 'device identifier' is NOT 0000.
From LoadKeyboardLayout() documentation:
The name of the input locale identifier to load. This name is a string composed of the hexadecimal value of the Language Identifier (low word) and a device identifier (high word). For example, U.S. English has a language identifier of 0x0409, so the primary U.S. English layout is named "00000409". Variants of U.S. English layout (such as the Dvorak layout) are named "00010409", "00020409", and so on.
If one creates a 'Custom' keyboard, I find it is impossible to get the 'device identifier' from the custom keyboards HKL.
For instance:
Create a Custom Keyboard US with MKLCS.
Install it.
The 'KLID' will be 'A0000409'.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\a0000409
The HKL will be 0xF0C00409 (as returned within the HKL list generated by GetKeyboardLayoutList()).
In order to load the keyboard with LoadKeyboardLayout(), one needs 'A0000409'.
It does not seem possible to create A0000409 from F0C00409.
I also created my own keyboard layout without MKLCS.
I arbitrarily named it 00060409.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\00060409
It's HKL is FFFE0409 (as returned within the HKL list generated by GetKeyboardLayoutList()).
It does not seem possible to create 00060409 from FFFE0409.
With all of that said, how does one obtain a KLID from an HKL?
Or, is there another way I can go about creating my list of installed keyboards?
//================================================================
11/25/2020 Addition.
Thank you Rita.
It seems as if GetKeyboardLayoutList() creates a list of 'Loaded' keyboard layouts.
System loaded keyboard layouts, 0x0409409,....
Keyboard Layouts installed via MKLCS installation.
It seems that any Keyboards defined within the following registry keys will get loaded at boot.
HKEY_CURRENT_USER\Keyboard Layout
Note the Preload and Substitute value's.
(Note there are many other 'Keyboard Layout' keys spread around within the registry, so I am
not sure if HKEY_CURRENT_USER\Keyboard Layout is the actual registry key that defines the
PreLoad - Substitutes.
HKEY_USERS\.DEFAULT\Keyboard Layout
HKEY_USERS\S-1-5-18 (My User Account)
HKEY_LOCAL_MACHINE\SYSTEM\Keyboard Layout
)
So there is no doubt in my mind that Rita's example works due to the fact that her HKEY_CURRENT_USER\Keyboard Layout contains the following entries:
Preload:
1 d0010804
Substitutes:
d0010804 a0000804
The entries may have been put there by the MKLCS installer?
Or perhaps the action of adding the keyboard via Settings->Time & Language->Click on Preferred Language->Options->Add a Keyboard
From ActivateKeyboardLayout() documentation:
The input locale identifier must have been loaded by a previous call to the LoadKeyboardLayout function.
Since a0000804 (HKL: F0C00804) is actually already loaded ActivateKeyboardLayout() works.
Since my KLID: 00060409 is not referenced within any of the X\Keyboard Layout Preload and Substitutes
I must physically call LoadKeyBoardLayout(L"00060409") in order for it to appear within the GetKeyboardList() HKL's.
Thanks again Rita.
how does one obtain a KLID from an HKL?
There seems no direct method out of box to achieve it.
A workaround is retrieving a list of HKLs, then active a layout via a HKL, then get its KLID. The following is an example:
int cnt = GetKeyboardLayoutList(sizeof(hklArr)/sizeof(hklArr[0]), hklArr);
if(cnt > 0)
{
printf("keyboard list: \n");
for (UINT i = 0; i < cnt; i++)
{
printf("%x\n", (LONG_PTR)hklArr[i]);
if (ActivateKeyboardLayout(hklArr[i], KLF_SETFORPROCESS))
{
WCHAR pName[KL_NAMELENGTH];
if (GetKeyboardLayoutName(pName))
{
wprintf(L"layout name (KLID): %s\n", pName);
}
}
}
}
Result like this:
Update:
Update 2: Share my creating and installing steps.
I use Keyboard Layout Creator 1.4.
Load an existing keyboard for testing purpose. (You can modify based on it or create your own completely.)
Valid and test keyboard to make sure it works as you expected. Then build DLL and setup package.
Run setup.exe generated by step 2. After installation complete you will see the related preload keyboard layout item in registry.
There is no safe way to do this since it is not documented and can be changed after Windows updates.
Proper algorithm is posted here.
Here is my code that is working on my Windows 10 Version 21H2 (November 2021 Update) so far:
// Returns KLID string of size KL_NAMELENGTH
// Same as GetKeyboardLayoutName but for any HKL
// https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values
BOOL GetKLIDFromHKL(HKL hkl, _Out_writes_(KL_NAMELENGTH) LPWSTR pwszKLID)
{
bool succeded = false;
if ((HIWORD(hkl) & 0xf000) == 0xf000) // deviceId contains layoutId
{
WORD layoutId = HIWORD(hkl) & 0x0fff;
HKEY key;
CHECK_EQ(::RegOpenKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", &key), ERROR_SUCCESS);
DWORD index = 0;
while (::RegEnumKeyW(key, index, pwszKLID, KL_NAMELENGTH) == ERROR_SUCCESS)
{
WCHAR layoutIdBuffer[MAX_PATH] = {};
DWORD layoutIdBufferSize = sizeof(layoutIdBuffer);
if (::RegGetValueW(key, pwszKLID, L"Layout Id", RRF_RT_REG_SZ, nullptr, layoutIdBuffer, &layoutIdBufferSize) == ERROR_SUCCESS)
{
if (layoutId == std::stoul(layoutIdBuffer, nullptr, 16))
{
succeded = true;
DBGPRINT("Found KLID 0x%ls by layoutId=0x%04x", pwszKLID, layoutId);
break;
}
}
++index;
}
CHECK_EQ(::RegCloseKey(key), ERROR_SUCCESS);
}
else
{
WORD langId = LOWORD(hkl);
// deviceId overrides langId if set
if (HIWORD(hkl) != 0)
langId = HIWORD(hkl);
std::swprintf(pwszKLID, KL_NAMELENGTH, L"%08X", langId);
succeded = true;
DBGPRINT("Found KLID 0x%ls by langId=0x%04x", pwszKLID, langId);
}
return succeded;
}
Also you can use this code to enumerate layout profiles with KLID in LAYOUTORTIPPROFILE.szId:
typedef struct tagLAYOUTORTIPPROFILE {
DWORD dwProfileType;
LANGID langid;
CLSID clsid;
GUID guidProfile;
GUID catid;
DWORD dwSubstituteLayout;
DWORD dwFlags;
WCHAR szId[MAX_PATH];
} LAYOUTORTIPPROFILE;
// Flags used in LAYOUTORTIPPROFILE::dwProfileType
#define LOTP_INPUTPROCESSOR 1
#define LOTP_KEYBOARDLAYOUT 2
// Flags used in LAYOUTORTIPPROFILE::dwFlags.
#define LOT_DEFAULT 0x0001
#define LOT_DISABLED 0x0002
std::vector<LAYOUTORTIPPROFILE> EnumLayoutProfiles()
{
// http://archives.miloush.net/michkap/archive/2008/09/29/8968315.html
// https://learn.microsoft.com/en-us/windows/win32/tsf/enumenabledlayoutortip
typedef UINT(WINAPI* EnumEnabledLayoutOrTipFunc)(LPCWSTR pszUserReg, LPCWSTR pszSystemReg, LPCWSTR pszSoftwareReg, LAYOUTORTIPPROFILE* pLayoutOrTipProfile, UINT uBufLength);
static EnumEnabledLayoutOrTipFunc EnumEnabledLayoutOrTip = reinterpret_cast<EnumEnabledLayoutOrTipFunc>(::GetProcAddress(::LoadLibraryA("input.dll"), "EnumEnabledLayoutOrTip"));
if (!EnumEnabledLayoutOrTip)
return {};
const UINT count = EnumEnabledLayoutOrTip(nullptr, nullptr, nullptr, nullptr, 0);
std::vector<LAYOUTORTIPPROFILE> layouts;
layouts.resize(count);
const UINT written = EnumEnabledLayoutOrTip(nullptr, nullptr, nullptr, layouts.data(), count);
CHECK_EQ(count, written);
return layouts;
}
The LAYOUTORTIPPROFILE.szId string format of the layout is:
<LangID>:<KLID>
The string format of the text service profile (IME) is:
<LangID>:{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
More info here.
here is the code that I have in OnPaint event of my form:
int elementCount;
String tStr = L"15:00";
::BeginPath(Canvas->Handle);
::TextOut(Canvas->Handle, 5, 5, tStr.c_str(), tStr.Length());
::EndPath(Canvas->Handle);
elementCount = ::GetPath(Canvas->Handle, NULL, NULL, 0);
Canvas->Brush->Color = clBlue;
Canvas->Pen->Color = clYellow;
Canvas->Pen->Width = 4;
if(0 < elementCount)
{
boost::scoped_array<TPoint> mPoints(new TPoint[elementCount]);
boost::scoped_array<BYTE> mTypes(new BYTE[elementCount]);
::GetPath(Canvas->Handle, mPoints.get(), mTypes.get(), elementCount);
::FillPath(Canvas->Handle);
::PolyDraw(Canvas->Handle, mPoints.get(), mTypes.get(), elementCount);
}
else
::StrokeAndFillPath(Canvas->Handle);
but here is what I get on the form:
as you can see the text comes out inverted (the text has to be blue and background gray but it is the other way around and the yellow line is around the background instead of text). Does anyone know how I can fix this?
I am using C++ Builder 10 Seattle but if anyone knows that Delphi or pure C++ trick, I can work with that as well.
Thank you
This is explained in TextOut's documentation:
When the TextOut function is placed inside a path bracket, the
system generates a path for the TrueType text that includes each
character plus its character box. The region generated is the
character box minus the text, rather than the text itself. You can
obtain the region enclosed by the outline of the TrueType text by
setting the background mode to transparent before placing the
TextOut function in the path bracket. Following is sample code that demonstrates this procedure.
The below is a Delphi adaption of the mentioned sample code and your snippet, draws yellow outlined blue text:
procedure TForm1.FormPaint(Sender: TObject);
var
elementCount: Integer;
mPoints: array of TPoint;
mTypes: array of Byte;
const
tStr = '15:00';
begin
BeginPath(Canvas.Handle);
Canvas.Brush.Style := bsClear;
TextOut(Canvas.Handle, 5, 5, PChar(tStr), Length(tStr));
EndPath(Canvas.Handle);
Canvas.Brush.Color := clBlue;
Canvas.Pen.Color := clYellow;
Canvas.Pen.Width := 4;
elementCount := GetPath(Canvas.Handle, Pointer(nil)^, Pointer(nil)^, 0);
if elementCount > 0 then begin
SetLength(mPoints, elementCount);
SetLength(mTypes, elementCount);
GetPath(Canvas.Handle, mPoints[0], mTypes[0], elementCount);
Canvas.Brush.Style := bsSolid;
SelectClipPath(Canvas.Handle, RGN_AND);
Canvas.FillRect(ClientRect);
SelectClipRgn(Canvas.Handle, 0);
PolyDraw(Canvas.Handle, mPoints[0], mTypes[0], elementCount);
end else
StrokeAndFillPath(Canvas.Handle);
end;
VS2010 introduced CMFCShellTreeCtrl which allows for a folder-browser tree ctrl to be dropped into our MFC apps.
However, there seems to be a serious lack of filtering capabilities in this class. i.e. it will build the list of container-objects (IShellFolder). But there doesn't seem to be a way to specify that .zip containers should not be displayed in the folder tree.
It does supply a virtual that could be used crudely for this purpose:
HRESULT CMFCShellTreeCtrl::EnumObjects(HTREEITEM hParentItem, LPSHELLFOLDER pParentFolder, LPITEMIDLIST pidlParent)
{
ASSERT_VALID(this);
ASSERT_VALID(afxShellManager);
LPENUMIDLIST pEnum = NULL;
HRESULT hr = pParentFolder->EnumObjects(NULL, m_dwFlags, &pEnum);
if (FAILED(hr) || pEnum == NULL)
{
return hr;
}
LPITEMIDLIST pidlTemp;
DWORD dwFetched = 1;
// Enumerate the item's PIDLs:
while (SUCCEEDED(pEnum->Next(1, &pidlTemp, &dwFetched)) && dwFetched)
{
TVITEM tvItem;
ZeroMemory(&tvItem, sizeof(tvItem));
// Fill in the TV_ITEM structure for this item:
tvItem.mask = TVIF_PARAM | TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_CHILDREN;
// AddRef the parent folder so it's pointer stays valid:
pParentFolder->AddRef();
// Put the private information in the lParam:
LPAFX_SHELLITEMINFO pItem = (LPAFX_SHELLITEMINFO)GlobalAlloc(GPTR, sizeof(AFX_SHELLITEMINFO));
ENSURE(pItem != NULL);
pItem->pidlRel = pidlTemp;
pItem->pidlFQ = afxShellManager->ConcatenateItem(pidlParent, pidlTemp);
pItem->pParentFolder = pParentFolder;
tvItem.lParam = (LPARAM)pItem;
CString strItem = OnGetItemText(pItem);
tvItem.pszText = strItem.GetBuffer(strItem.GetLength());
tvItem.iImage = OnGetItemIcon(pItem, FALSE);
tvItem.iSelectedImage = OnGetItemIcon(pItem, TRUE);
// Determine if the item has children:
DWORD dwAttribs = SFGAO_HASSUBFOLDER | SFGAO_FOLDER | SFGAO_DISPLAYATTRMASK | SFGAO_CANRENAME | SFGAO_FILESYSANCESTOR;
pParentFolder->GetAttributesOf(1, (LPCITEMIDLIST*) &pidlTemp, &dwAttribs);
tvItem.cChildren = (dwAttribs & (SFGAO_HASSUBFOLDER | SFGAO_FILESYSANCESTOR));
// Determine if the item is shared:
if (dwAttribs & SFGAO_SHARE)
{
tvItem.mask |= TVIF_STATE;
tvItem.stateMask |= TVIS_OVERLAYMASK;
tvItem.state |= INDEXTOOVERLAYMASK(1); //1 is the index for the shared overlay image
}
// Fill in the TV_INSERTSTRUCT structure for this item:
TVINSERTSTRUCT tvInsert;
tvInsert.item = tvItem;
tvInsert.hInsertAfter = TVI_LAST;
tvInsert.hParent = hParentItem;
InsertItem(&tvInsert);
dwFetched = 0;
}
pEnum->Release();
return S_OK;
}
What I am confused by is the lack of ability to distinguish what type of object this is that is being enumerated (or a better way to control the enumeration in the first place so as to filter out non-filesystem objects such as these).
It's possible to look at the text for the item being enumerated, and simply exclude it if it ends in ".zip". However, this seems wonky to me. After all, an arbitrary folder could be named XYZ.zip (which still being a folder, and not a zip archive). Similarly, there are likely other archive types than just .zip that may be yet supported in the future - or other container types which are nevertheless not really folders.
I don't want to eliminate things like "Network" and "Computer" from being valid nodes. Just ones that are problematic such as "Downloads\Foobar.zip"
I apologize for this question being rambling. Any help in improving my understanding and creative approaches to knowing what sorts of objects are part of Microsoft's shell namespace, and how that they can be gainfully used would be appreciated!
A zip file/folder would have SFGAO_STREAM/SFGAO_DROPTARGET along with SFGAO_FOLDER, so if you can read the shell item as stream, then it probably isn't a directory. Another way to tell is to use SHGetPathFromIDList+PathIsDirectory, however this only works for pidl that have file system paths.
There are other kind of browsable files too, like a saved search (and if you browse into the file it takes like forever to finish enumerating the items), so you probably want to think about how to deal with those files too.
As you know many ui components and dev tools doesn't support rtl , we can call it flipping text , cause result is same example :
LTR
سلام salam متن راهنما word
RTL
word متن راهنما salam سلام
is there anyway to convert this LTR to RTL , i don't have any idea and language doesn't matter
Actually i am seeking for a solution to get this done in RAD Studio Firemonkey Application , as you may know firemonkey apps doesn't support rtl it's in roadmap of rad studio but not implemented yet
Under Windows, you can do that via the UniScribe API.
I've used this to convert Unicode text into set of glyphs, for our Open Source PDF writer.
You have source code sample in SynPdf.pas unit. See the TPdfWrite.AddUnicodeHexTextUniScribe method:
function TPdfWrite.AddUnicodeHexTextUniScribe(PW: PWideChar;
WinAnsiTTF: TPdfFontTrueType; NextLine: boolean; Canvas: TPdfCanvas): boolean;
var L, i,j: integer;
res: HRESULT;
max, count, numSp: integer;
Sp: PScriptPropertiesArray;
W: PWideChar;
items: array of TScriptItem;
level: array of byte;
VisualToLogical: array of integer;
psc: pointer; // opaque Uniscribe font metric cache
complex,R2L: boolean;
complexs: array of byte;
glyphs: array of TScriptVisAttr;
glyphsCount: integer;
OutGlyphs, LogClust: array of word;
procedure Append(i: Integer);
// local procedure used to add glyphs from items[i] to the PDF content stream
var L: integer;
W: PWideChar;
procedure DefaultAppend;
var tmpU: array of WideChar;
begin
SetLength(tmpU,L+1); // we need the text to be ending with #0
move(W^,tmpU[0],L*2);
AddUnicodeHexTextNoUniScribe(pointer(tmpU),WinAnsiTTF,false,Canvas);
end;
begin
L := items[i+1].iCharPos-items[i].iCharPos; // length of this shapeable item
if L=0 then
exit; // nothing to append
W := PW+items[i].iCharPos;
if not GetBit(complexs[0],i) then begin
// not complex items are rendered as fast as possible
DefaultAppend;
exit;
end;
res := ScriptShape(0,psc,W,L,max,#items[i].a,
pointer(OutGlyphs),pointer(LogClust),pointer(glyphs),glyphsCount);
case res of
E_OUTOFMEMORY: begin // max was not big enough (should never happen)
DefaultAppend;
exit;
end;
E_PENDING, USP_E_SCRIPT_NOT_IN_FONT: begin // need HDC and a selected font object
res := ScriptShape(Canvas.FDoc.GetDCWithFont(WinAnsiTTF),
psc,W,L,max,#items[i].a,
pointer(OutGlyphs),pointer(LogClust),pointer(glyphs),glyphsCount);
if res<>0 then begin // we won't change font if necessary, sorry
// we shall implement the complex technic as stated by
// http://msdn.microsoft.com/en-us/library/dd374105(v=VS.85).aspx
DefaultAppend;
exit;
end;
end;
0: ; // success -> will add glyphs just below
else exit;
end;
// add glyphs to the PDF content
// (NextLine has already been handled: not needed here)
AddGlyphs(pointer(OutGlyphs),glyphsCount,Canvas);
end;
begin
result := false; // on UniScribe error, handle as Unicode
// 1. Breaks a Unicode string into individually shapeable items
L := StrLenW(PW)+1; // include last #0
max := L+2; // should be big enough
SetLength(items,max);
count := 0;
if ScriptItemize(PW,L,max,nil,nil,pointer(items),count)<>0 then
exit; // error trying processing Glyph Shaping -> fast return
// 2. guess if requiring glyph shaping or layout
SetLength(complexs,(count shr 3)+1);
ScriptGetProperties(sP,numSp);
complex := false;
R2L := false;
for i := 0 to Count-2 do // don't need Count-1 = Terminator
if fComplex in sP^[items[i].a.eScript and (1 shl 10-1)]^.fFlags then begin
complex := true;
SetBit(complexs[0],i);
end else
if fRTL in items[i].a.fFlags then
R2L := true;
if not complex then begin
// no glyph shaping -> fast append as normal Unicode Text
if R2L then begin
// handle Right To Left but not Complex text
W := pointer(items); // there is enough temp space in items[]
W[L] := #0;
dec(L);
for i := 0 to L do
W[i] := PW[L-i];
AddUnicodeHexTextNoUniScribe(W,WinAnsiTTF,NextLine,Canvas);
result := true; // mark handled here
end;
exit;
end;
// 3. get Visual Order, i.e. how to render the content from left to right
SetLength(level,count);
for i := 0 to Count-1 do
level[i] := items[i].a.s.uBidiLevel;
SetLength(VisualToLogical,count);
if ScriptLayout(Count,pointer(level),pointer(VisualToLogical),nil)<>0 then
exit;
// 4. now we have enough information to start drawing
result := true;
if NextLine then
Canvas.MoveToNextLine; // manual NextLine handling
// 5. add glyphs for all shapeable items
max := (L*3)shr 1+32; // should be big enough - allocate only once
SetLength(glyphs,max);
SetLength(OutGlyphs,max);
SetLength(LogClust,max);
psc := nil; // cached for the same character style used
if Canvas.RightToLeftText then
// append from right to left visual order
for j := Count-2 downto 0 do // Count-2: ignore last ending item
Append(VisualToLogical[j]) else
// append from left to right visual order
for j := 0 to Count-2 do // Count-2: ignore last ending item
Append(VisualToLogical[j]);
end;
Of course, this is under Windows only. So it won't work on Mac OS X. You'll have to use another library under Mac OS X...
It's complicated. If you want to do it correctly, you must use the Bidi Library from the International Components for Unicode.
If you use MFC, here is how to set both righting direction and alignment. Assuming your CEdit control is named m_TextEdit:
void MyDialog::SetLangDirection(bool RTL)
{
DWORD w_dwStyle;
w_dwStyle = GetWindowLong(m_TextEdit.GetSafeHwnd(), GWL_EXSTYLE);
if (RTL)
{
w_dwStyle -= WS_EX_LEFT | WS_EX_LTRREADING;
w_dwStyle |= WS_EX_RIGHT | WS_EX_RTLREADING;
}
else
{
w_dwStyle -= WS_EX_RIGHT | WS_EX_RTLREADING;
w_dwStyle |= WS_EX_LEFT | WS_EX_LTRREADING;
}
SetWindowLong(m_TextEdit.GetSafeHwnd(), GWL_EXSTYLE, w_dwStyle);
}
See my tip.
I'm having a C++ application connecting to the MS SQL Server 2005 using CDynamicAccessor.
When my column is text or ntext, the CDynamicAccessor::GetColumnType always return DBTYPE_IUNKNOWN. I can bind the data as ISequentialStream and read the byte stream out. However, how can I tell if the column that I'm reading is text or ntext, and hence, to interpret the byte stream as ASCII or Unicode?
The CDynamicAccessor class overwrites the original DBTYPE value of the column with DBTYPE_UNKNOWN. In order to get the original DBTYPE, we need to call the GetColumnInfo function. An example of how to do this is the DynamicConsumer sample application included in the Visual Studio 2008 samples:
// the following case will handle BLOBs binded as ISequentialStream/IStream pointer
if( dbtype == DBTYPE_IUNKNOWN )
{
// first we have to determine what was the column's type originally reported by the provider
CComHeapPtr<DBCOLUMNINFO> spColumnInfo;
CComHeapPtr<OLECHAR> spStringsBuffer;
DBORDINAL nColumns = 0;
HRESULT hres = rs.CDynamicAccessor::GetColumnInfo( rs.m_spRowset, &nColumns, &spColumnInfo, &spStringsBuffer );
ATLASSERT( SUCCEEDED( hres ) );
ATLASSERT( col <= nColumns );
DBTYPE wType = spColumnInfo[col-1].wType;
...
}
The alternative solution is to set the CDynamicAccessor to DBBLOBHANDLING_NOSTREAMS, which seems to result in much less complicated code to handle data loading (you can see this in the full VS2008 sample code).
However, I'm not sure why using stream is the default option, and if there is any disadvantages with using no streams.