My problem is this: My SAPI inproc recognizer refuses to load my grammar file when the windows 10 Display language is set to English(UK).
System Display language set to UK. Speech Recognition language is UK. System Locale is UK. SAPI xml-format Grammar even specifies LANGID=809 - as far as I can tell, EVERYTHING is set to EN-GB, and yet the grammar still won't load.
But it loads and works just fine when display language and is set to English(US).
Does anyone know what's up with this? It's extremely frustrating... Hopefully I'm just missing something simple.
SAPI Initialization code:
//////////////INITIALIZE SAPI ENGINE AND GRAMMAR//////////////////////////////
HRESULT SpeechObject::Initialize(){
//INITIALIZE SR ENGINE
if (FAILED(test=::CoInitialize(NULL)))
SRError(L"COM Initialization Fail");
//Create recognizer instance
if (FAILED(test=cpEngine.CoCreateInstance(CLSID_SpInprocRecognizer))){
SRError(L"Can't Load Reco Engine");
return test;
}
//Load the audio Input (in seperate function to facilitate reload)
LoadAudio(); //should I check this?
//load Default recognizer settings
cpEngine->SetRecognizer(NULL);
//get and load default reco profile
if (FAILED(SpGetDefaultTokenFromCategoryId(SPCAT_RECOPROFILES, &cpObjectToken)))
SRError(L"Can't Find Recognition Profile");
if (FAILED(cpEngine->SetRecoProfile(cpObjectToken)))
SRError(L"Can't Load Recognition Profile");
//create reco context
if (FAILED(test=cpEngine->CreateRecoContext(&cpContext))){
SRError(L"Can't Create Reco Context");
return test;
}
//send pSpeechObject to global callback function
cpContext->SetNotifyCallbackFunction(
(SPNOTIFYCALLBACK*)SpeechCallBack,
NULL, (LPARAM)this);
if(FAILED(cpContext->CreateGrammar(NULL, &cpGrammar)))
SRError(L"Can't Create context");
char str[80]; ////TEST
sprintf(str, "LANGID: %X", GetUserDefaultUILanguage());
MessageBoxA(GetActiveWindow(), str,0,0);
//load grammar from compiled grammar resource
if (FAILED(test = cpGrammar->LoadCmdFromResource(
hModule, MAKEINTRESOURCE(GRAMMARCFG),
L"FILE", GetUserDefaultUILanguage(), SPLO_STATIC))){
SRError(L"Can't Load Grammar. Please check language settings");
return test;
}
//(comment above and uncomment following to load from raw xml file for testing)
//cpGrammar->LoadCmdFromFile(L"Grammar.xml", SPLO_STATIC);
//Enable Engine and Reco Context
cpEngine->SetRecoState(SPRST_ACTIVE);
cpContext->SetContextState(SPCS_ENABLED);
//enable ALWAYS ACTIVE and GROUND ENGINES ON commands
return(cpGrammar->SetRuleState(NULL, NULL, SPRS_ACTIVE));
}
////////////LOAD (AND RELOAD) AUDIO INPUT//////////////////////
HRESULT SpeechObject::LoadAudio(bool dlgFlag){
if (FAILED(test = SpCreateDefaultObjectFromCategoryId(SPCAT_AUDIOIN, &cpAudioIn))){
SRError(L"Can't Find Default Audio Input");
return test;
}
if (FAILED(test = cpEngine->SetInput(cpAudioIn, TRUE))){
if (!dlgFlag)
SRError(L"Can't Set Audio Input");
return test;
}
if (pSRDisplay)
pSRDisplay->DisplayText("Audio Reloaded");
if (pDLog)
pDLog->LogEvent("Audio Reloaded");
//RecoState must be reenabled after audio reset
cpEngine->SetRecoState(SPRST_ACTIVE);
if (pDLog)
pDLog->LogEvent("SR ENABLED");
return test;
}
I get the "Can't Load Grammar. Please check language settings" error any time the display language is not English(US), even if I confirm that ALL SETTINGS match...
Would really appreciate any sort of insight from persons more knowledgeable than I.
Farley
You need to explicitly load the recognizer for your preferred language. In particular, this:
//load Default recognizer settings
cpEngine->SetRecognizer(NULL);
always loads the recognizer specified in the speech control panel. You likely want something like this:
CComPtr<ISpObjectToken> cpEngineToken;
hr = SpFindBestToken(SPCAT_RECOGNIZERS, L"Language=<hex language id>", NULL, &cpEngineToken);
// check hr
hr = cpEngine->SetRecognizer(cpEngineToken);
where you'll need to convert the LCID from GetUserDefaultUILanguage to a hex number.
The issue was that I had misinterpreted the meaning of the "language" parameter in LoadCmdFromResource(). I'll blame it on the ambiguous SAPI documentation, though if I had experience loading some other types of resources before I might have been tipped off to this. ;) I had thought it was somehow used by SAPI and should match the language of the system and recognizer (that's what it sounded like in the documentation). Turns out, it actually just specifies the language used to compile the .RC file the grammar is included in (presumably to allow multiple translations to be included in separate .rc's).
The code works perfectly as originally posted, so long as I replace "GetUserDefaultUI()" with an explicit "0x409" (the language specified in the resource compiler) in the call to LoadCmdFromResource(). Now it works with US English, UK English and presumably all English recognizers, and loads the recognizer selected in the speech control panel regardless of the Display language setting (which can even be non-English).
Many, many thanks to Eric Brown for tipping me off to this, I was starting to lose my mind.
Farley
Related
The Q&A is currently a subject of meta discussion, do participate. The current plan is to split where possible into Q&As. Answers to the A&A are community wiki and the question should become one when the status is resolved.
Preface
This Q&A strives to become a collection and a reference target for common errors encountered during development in Google Apps Script language in hopes to improve long-term maintainability of google-apps-script tag.
There are several similar and successful undergoings in other languages and general-purpose tags (see c++, android, php, php again), and this one follows suit.
Why it exists?
The amount of questions from both new and experienced developers regarding the meaning and solutions to errors encountered during development and production that can be effectively reduced to a single answer is substantial. At the time of writing, even running a query only by language tag yields:
"Cannot find method" 8 pages
"Cannot read property" 9 pages
"Cannot call ... in this context" 5 pages
"You do not have permission" 11 pages
Linking to a most relevant duplicate is hard and time-consuming for volunteers due to the need to consider nuances and often poorly-worded titles.
What it consists of?
Entries in this Q&A contain are designed to provide info on how to:
parse the error message structure
understand what the error entails
consistently reproduce (where applicable)
resolve the issue
provide a link to canonical Q&A (where possible)
Table of Contents
To help you navigate the growing reference please use the TOC below:
General errors
Service-specific errors
What this is not?
The scope of the Q&A is limited to common (not trivial). This is not:
a catch-all guide or "best practices" collection
a reference for general ECMAScript errors
GAS documentation
a resources list (we have a tag wiki for that)
What to add?
When adding an entry, please, consider the following:
is the error common enough (see "why" section for examples)?
can the solution be described concisely and be applicable for most cases?
Preface
The answer provides a guide on general errors that can be encountered when working with any Google service (both built-in and advanced) or API. For errors specific to certain services, see the other answer.
Back to reference
General errors
Message
TypeError: Cannot read property 'property name here' from undefined (or null)
Description
The error message indicates that you are trying to access a property on an Object instance, but during runtime the value actually held by a variable is a special data type undefined. Typically, the error occurs when accessing nested properties of an object.
A variation of this error with a numeric value in place of property name indicates that an instance of Array was expected. As arrays in JavaScript are objects, everything mentioned here is true about them as well.
There is a special case of dynamically constructed objects such as event objects that are only available in specific contexts like making an HTTP request to the app or invoking a function via time or event-based trigger.
The error is a TypeError because an "object" is expected, but "undefined" is received
How to fix
Using default values
Logical OR || operator in JavaScript has an intersting property of evaluating the right-hand side iff the left-hand is falsy. Since objects in JS are truthy, and undefined and null are falsy, an expression like (myVar || {}).myProp [(myVar || [])[index] for arrays] will guarantee that no error is thrown and the property is at least undefined.
One can also provide default values: (myVar || { myProp : 2 }) guarantees accessing myProp to return 2 by default. Same goes for arrays: (myVar || [1,2,3]).
Checking for type
Especially true for the special case, typeof operator combined with an if statement and a comparison operator will either allow a function to run outside of its designated context (i.e. for debugging purposes) or introduce branching logic depending on whether the object is present or not.
One can control how strict the check should be:
lax ("not undefined"): if(typeof myVar !== "undefined") { //do something; }
strict ("proper objects only"): if(typeof myVar === "object" && myVar) { //do stuff }
Related Q&As
Parsing order of the GAS project as the source of the issue
Message
Cannot convert some value to data type
Description
The error is thrown due to passing an argument of different type than a method expects. A common mistake that causes the error is accidental coercion of a number to string.
How to reproduce
function testConversionError() {
const ss = SpreadsheetApp.getActiveSheet();
ss.getRange("42.0",1);
}
How to fix
Make sure that the value referenced in the error message is of data type required by documentation and convert as needed.
Message
Cannot call Service and method name from this context
Description
This error happens on a context mismatch and is specific to container-bound scripts.
The primary use case that results in the error is trying to call a method only available in one document type (usually, getUi() as it is shared by several services) from another (i.e. DocumentApp.getUi() from a spreadsheet).
A secondary, but also prominent case is a result of calling a service not explicitly allowed to be called from a custom function (usually a function marked by special JSDoc-style comment #customfunction and used as a formula).
How to reproduce
For bound script context mismatch, declare and run this function in a script project tied to Google Sheets (or anything other than Google Docs):
function testContextMismatch() {
const doc = DocumentApp.getUi();
}
Note that calling a DocumentApp.getActiveDocument() will simply result in null on mismatch, and the execution will succeed.
For custom functions, use the function declared below in any cell as a formula:
/**
* #customfunction
*/
function testConversionError() {
const ui = SpreadsheetApp.getUi();
ui.alert(`UI is out of scope of custom function`);
}
How to fix
Context mismatch is easily fixed by changing the service on which the method is called.
Custom functions cannot be made to call these services, use custom menus or dialogs.
Message
Cannot find method Method name here
The parameters param names do not match the method signature for method name
Description
This error has a notoriously confusing message for newcomers. What it says is that a type mismatch occurred in one or more of the arguments passed when the method in question was called.
There is no method with the signature that corresponds to how you called it, hence "not found"
How to fix
The only fix here is to read the documentation carefully and check if order and inferred type of parameters are correct (using a good IDE with autocomplete will help). Sometimes, though, the issue happens because one expects the value to be of a certain type while at runtime it is of another. There are several tips for preventing such issues:
Setting up type guards (typeof myVar === "string" and similar).
Adding a validator to fix the type dynamically thanks to JavaScript being dynamically typed.
Sample
/**
* #summary pure arg validator boilerplate
* #param {function (any) : any}
* #param {...any} args
* #returns {any[]}
*/
const validate = (guard, ...args) => args.map(guard);
const functionWithValidator = (...args) => {
const guard = (arg) => typeof arg !== "number" ? parseInt(arg) : arg;
const [a,b,c] = validate(guard, ...args);
const asObject = { a, b, c };
console.log(asObject);
return asObject;
};
//driver IIFE
(() => {
functionWithValidator("1 apple",2,"0x5");
})()
Messages
You do not have permission to perform that action
The script does not have permission to perform that action
Description
The error indicates that one of the APIs or services accessed lacks sufficient permissions from the user. Every service method that has an authorization section in its documentation requires at least one of the scopes to be authorized.
As GAS essentially wraps around Google APIs for development convenience, most of the scopes listed in OAuth 2.0 scopes for APIs reference can be used, although if one is listed in the corresponding docs it may be better to use it as there are some inconsistencies.
Note that custom functions run without authorization. Calling a function from a Google sheet cell is the most common cause of this error.
How to fix
If a function calling the service is ran from the script editor, you are automatically prompted to authorize it with relevant scopes. Albeit useful for quick manual tests, it is best practice to set scopes explicitly in application manifest (appscript.json). Besides, automatic scopes are usually too broad to pass the review if one intends to publish the app.
The field oauthScopes in manifest file (View -> Show manifest file if in code editor) should look something like this:
"oauthScopes": [
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/userinfo.email",
//etc
]
For custom functions, you can fix it by switching to calling the function from a menu or a button as custom functions cannot be authorized.
For those developing editor Add-ons, this error means an unhandled authorization lifecycle mode: one has to abort before calls to services that require authorization in case auth mode is AuthMode.NONE.
Related causes and solutions
#OnlyCurrentDoc limiting script access scope
Scopes autodetection
Message
ReferenceError: service name is not defined
Description
The most common cause is using an advanced service without enabling it. When such a service is enabled, a variable under the specified identifier is attached to global scope that the developer can reference directly. Thus, when a disabled service is referenced, a ReferenceError is thrown.
How to fix
Go to "Resources -> Advanced Google Services" menu and enable the service referenced. Note that the identifier should equal the global variable referenced.
For a more detailed explanation, read the official guide.
If one hasn't referenced any advanced services then the error points to an undeclared variable being referenced.
Message
The script completed but did not return anything.
Script function not found: doGet or doPost
Description
This is not an error per se (as the HTTP response code returned is 200 and the execution is marked as successful, but is commonly regarded as one. The message appears when trying to make a request/access from browser a script deployed as a Web App.
There are two primary reasons why this would happen:
There is no doGet or doPost trigger function
Triggers above do not return an HtmlOutput or TextOutput instance
How to fix
For the first reason, simply provide a doGet or doPost trigger (or both) function. For the second, make sure that all routes of your app end with creation of TextOutput or HtmlOutput:
//doGet returning HTML
function doGet(e) {
return HtmlService.createHtmlOutput("<p>Some text</p>");
}
//doPost returning text
function doPost(e) {
const { parameters } = e;
const echoed = JSON.stringify(parameters);
return ContentService.createTextOutput(echoed);
}
Note that there should be only one trigger function declared - treat them as entry points to your application.
If the trigger relies on parameter / parameters to route responses, make sure that the request URL is structured as "baseURL/exec?query" or "baseURL/dev?query" where query contains parameters to pass.
Related Q&As
Redeploying after declaring triggers
Message
We're sorry, a server error occurred. Please wait a bit and try again.
Description
This one is the most cryptic error and can occur at any point with nearly any service (although DriveApp usage is particularly susceptible to it). The error usually indicates a problem on Google's side that either goes away in a couple of hours/days or gets fixed in the process.
How to fix
There is no silver bullet for that one and usually, there is nothing you can do apart from filing an issue on the issue tracker or contacting support if you have a GSuite account. Before doing that one can try the following common remedies:
For bound scripts - creating a new document and copying over the existing project and data.
Switch to using an advanced Drive service (always remember to enable it first).
There might be a problem with a regular expression if the error points to a line with one.
Don't bash your head against this error - try locating affected code, file or star an issue and move on
Syntax error without apparent issues
This error is likely to be caused by using an ES6 syntax (for example, arrow functions) while using the deprecated Rhino runtime (at the time of writing the GAS platform uses V8).
How to fix
Open "appscript.json" manifest file and check if runtimeVersion is set to "V8", change it if not, or remove any ES6 features otherwise.
Quota-related errors
There are several errors related to quotas imposed on service usage. Google has a comprehensive list of those, but as a general rule of thumb, if a message matches "too many" pattern, you are likely to have exceeded the respective quota.
Most likely errors encountered:
Service invoked too many times: service name
There are too many scripts running
Service using too much computer time for one day
This script has too many triggers
How to fix
In most cases, the only fix is to wait until the quota is refreshed or switch to another account (unless the script is deployed as a Web App with permission to "run as me", in which case owner's quotas will be shared across all users).
To quote documentation at the time:
Daily quotas are refreshed at the end of a 24-hour window; the exact time of this refresh, however, varies between users.
Note that some services such as MailApp have methods like getRemainingDailyQuota that can check the remaining quota.
In the case of exceeding the maximum number of triggers one can check how many are installed via getProjectTriggers() (or check "My triggers" tab) and act accordingly to reduce the number (for example, by using deleteTrigger(trigger) to get rid of some).
Related canonical Q&As
How are daily limitations being applied and refreshed?
"Maximum execution time exceeded" problem
Optimizing service calls to reduce execution time
References
How to make error messages more meaningful
Debugging custom functions
Service-specific errors
The answer concerns built-in service-related errors. For general reference see the other answer. Entries addressing issues with services listed in official reference are welcome.
Back to reference
SpreadsheetApp
The number of rows in the range must be at least 1
This error is usually caused by calling the getRange method where the parameter that sets the number of rows happens to equal to 0. Be careful if you depend on getLastRow() call return value - only use it on non-empty sheets (getDataRange will be safer).
How to reproduce
sh.getRange(1, 1, 0, sh.getLastColumn()); //third param is the number of rows
How to fix
Adding a guard that prevents the value from ever becoming 0 should suffice. The pattern below defaults to the last row with data (optional if you only need a certain number of rows) and to 1 if that also fails:
//willFail is defined elsewhere
sh.getRange(1, 1, willFail || sh.getLastRow() || 1, sh.getLastColumn());
Error: “Reference does not exist”
The error happens when calling a custom function in a spreadsheet cell that does not return a value. The docs do mention only that one "must return a value to display", but the catch here is that an empty array is also not a valid return value (no elements to display).
How to reproduce
Call the custom function below in any Google Sheets spreadsheet cell:
/**
* #customfunction
*/
const testReferenceError = () => [];
How to fix
No specific handling is required, just make sure that length > 0.
The number of rows or cells in the data does not match the number of rows or cells in the range. The data has N but the range has M.
Description
The error points to a mismatch in dimensions of range in relation to values. Usually, the issue arises when using setValues() method when the matrix of values is smaller or bigger than the range.
How to reproduce
function testOutOfRange() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getActiveSheet();
const rng = sh.getActiveRange();
const vals = rng.getValues();
try {
vals.push([]);
rng.setValues(vals);
} catch (error) {
const ui = SpreadsheetApp.getUi();
ui.alert(error.message);
}
}
How to fix
If it is routinely expected for values to get out of bounds, implement a guard that catches such states, for example:
const checkBounds = (rng, values) => {
const targetRows = rng.getHeight();
const targetCols = rng.getWidth();
const { length } = values;
const [firstRow] = values;
return length === targetRows &&
firstRow.length === targetCols;
};
The coordinates of the range are outside the dimensions of the sheet.
Description
The error is a result of a collision between two issues:
The Range is out of bounds (getRange() does not throw on requesting a non-existent range)
Trying to call a method on a Range instance referring to a non-existent dimension of the sheet.
How to reproduce
function testOB() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getActiveSheet();
const rng = sh.getRange(sh.getMaxRows() + 1, 1);
rng.insertCheckboxes();
}
How to fix
Check that number of rows (getMaxRow()) and columns (getMaxColumns()) are both greater or equal to the parameters passed to getRange() method call and change them accordingly.
Exception: You can't create a filter in a sheet that already has a filter.
Description
The message means that you are trying to call a createFilter method on a Range in a Sheet that already has a filter set (either via UI or script), thus violating the restriction on 1 filter per Sheet, to quote the documentation:
There can be at most one filter in a sheet.
How to reproduce
const testFilterExistsError = () => {
const sh = SpreadsheetApp.getActiveSheet();
const rng = sh.getDataRange();
const filter1 = rng.createFilter();
const filter2 = rng.createFilter();
};
How to fix
Add a guard that checks for the existence of the filter first. getFilter returns either a filter or null if called on a Range instance and is perfect for the job:
const testFilterGuard = () => {
const sh = SpreadsheetApp.getActiveSheet();
const rng = sh.getDataRange();
const filter = rng.getFilter() || rng.createFilter();
//do something useful;
};
UrlFetchApp
Attribute provided with no value: url
Description
The error is specific to UrlFetchApp service and happens when fetch or fetchAll method gets called with an empty string or non-string value.
How to reproduce
const response = UrlFetchApp.fetch("", {});
How to fix
Make sure that a string containing a URI (not necessarily valid) is passed to the method as its first argument. As its common root cause is accessing a non-existent property on an object or array, check whether your accessors return an actual value.
I'm building a NativeScript plugin for iOS to integrate a card payment terminal as an external accessory. It is almost done, and working, but I have problem with passing one argument called "optionals". This is the whole code I'm trying to implement. It's the payworks framework for a Miura terminal. http://www.payworks.mpymnt.com/node/143
MPTransactionParameters *tp = [MPTransactionParameters chargeWithAmount:[NSDecimalNumber decimalNumberWithString:#"5.00"]
currency:MPCurrencyEUR
optionals:^(id<MPTransactionParametersOptionals> _Nonnull optionals) {
optionals.subject = #"Bouquet of Flowers";
optionals.customIdentifier = #"yourReferenceForTheTransaction";
}];
I cannot find a way of sending this "optionals" function.
In the generate typing metadata I see the MPTransactionParametersOptionals is a #protocol, but still don't know how to use it here as a parameter.
This is my current javascript code for the block
const tp = MPTransactionParameters.chargeWithAmountCurrencyOptionals(
amount,
MPCurrencyEUR,
function (optionals) {
console.log(optionals); //logs the newly created MPTransactionParameters instance, with set amount and currency properties, but cannot touch or set the optional properties.
}
);
The 3rd parameter of chargeWithAmountCurrencyOptionals() should be a function, but I'm doing it wrong, and searched everywhere in google how to do it but no success. I'm already trying for 2 days.
It is working, when the 3rd parameter is null, but I need the set the optional properties.
EDIT: adding the metadata. There are a lot of typings for MPtransactionParameters, so I decided to give you the whole file so you can search.
https://drive.google.com/open?id=1kvDoXtGbCoeCT20b9_t2stc2Qts3VyQx
EDIT2: Adding the typings:
https://drive.google.com/open?id=1lZ3ULYHbX7DXdUQMPoZeSfyEZrjItSOS
I was trying to use gl_FragDepthEXT in a shader but ran into issues. Is there something I have to do to enable this extension?
Yes, you are missing one requirement, when you are using a raw shader you must enable with the following string in your shader code:
"#extension GL_EXT_frag_depth : enable"
When using a THREE.ShaderMaterial the program string is partly auto-generated therefore the above string cannot be added early enough in your shader strings to avoid a shader compiler error so you enable with:
material.extensions.fragDepth = true
This will make gl_FragDepthEXT available as a fragment shader output if the extension is supported.
It is your hardware that determines if an extension is supported or not. So what you can do is query the hardware to see if the extension is supported. If you look in the source of three.js (src/renderers/webgl/WebGLExtensions.js), there are helper function to determine if an extension is supported:
// assuming here that _gl is the webgl context
var extensions = new THREE.WebGLExtensions( _gl );
// the return value is null if the extension is not supported,
// or otherwise an extension object
extensions.get( "gl_FragDepthEXT" );
or in pure webGL:
// returns an array of strings, one for each supported extension
// for informational purposes only
var available_extensions = _gl.getSupportedExtensions();
// the return value is null if the extension is not supported,
// or otherwise an extension object
var object_ext = _gl.getExtension( "gl_FragDepthEXT" );
Answering your question from the comments above, re: how well extension is supported. You can check http://webglstats.com/ to get a idea of webgl extensions currently supported by Devices/OS/Browser. The data comes from visitors on the participating websites only, but it should give you a general idea.
How do i get the shell IPreviewHandler for a particular file extension?
Background
Windows allows developers to create a preview handler for their custom file types:
Preview handlers are called when an item is selected to show a lightweight, rich, read-only preview of the file's contents in the view's reading pane. This is done without launching the file's associated application.
A preview handler is a hosted application. Hosts include the Windows Explorer in Windows Vista or Microsoft Outlook 2007.
I want to leverage the existing IPreviewHandler infrasturcture to get a thumbnail for a file.
In A Stream
The problem is that my files are not housed in the shell namespace (i.e. they are not sitting on the hard drive). They are sitting in memory, accessable through an IStream. This means i cannot use the legacy IExtractImage interface; as it does not support loading a file from a Stream.
Fortunately, this is why the modern IPreviewHandler supports (recommends, and prefers) loading data from a Stream, and recommends against loading previews from a file:
This method is preferred to Initialize due to its ability to use streams that are not accessible through a Win32 path, such as the contents of a compressed file with a .zip file name extension.
So how do i get it?
There is no documentation on the correct way to get ahold of the IPreviewHandler associated with a particular extension. But if i take the directions of how to register an IPreviewHandler, and read the contract from the other side:
HKEY_CLASSES_ROOT
.xyz
(Default) = xyzfile
HKEY_CLASSES_ROOT
xyzfile
shellex
{8895b1c6-b41f-4c1c-a562-0d564250836f} //IPreviewHandler subkey
(Default) = [clsid of the IPreviewHandler]
I should be able to follow the same route, given that i know the extension. Lets follow that with a real world example, a .jpg file:
Notice that the file has a preview. Notice i included the second screenshot only to reinforce the idea that the preview doesn't come from a file sitting on the hard drive.
Lets get spellunking!
First is the fact that it's a .jpg file:
HKEY_CLASSES_ROOT
.jpg
(Default) = ACDC_JPG
HKEY_CLASSES_ROOT
ACDC_JPG
ShellEx
{BB2E617C-0920-11d1-9A0B-00C04FC2D6C1}
ContextMenuHandlers
Wait, there is no {8895b1c6-b41f-4c1c-a562-0d564250836f} subkey for a previewhandler. That must mean that we cannot get a thumbnail for .jpg files.
reducto an absurdum
The Real Question
The careful reader will realize that the actual question i'm asking is:
How do i get the preview of an image contained only in a stream?
And while that is a useful question, and the real issue i'm having, having an answer on how to use IPreviewHandler is also a useful question.
So feel free to answer either; or both!
Bonus Reading
MSDN: Preview Handlers and Shell Preview Host
MSDN: How to Register a Preview Handler
MSDN: IInitializeWithStream::Initialize method
IPreviewHandler throws uncatchable exception
Outlook IPreviewHandler for Delphi
#hvd had the right answer.
File types have a ShellEx key, with {guid} subkeys. Each {guid} key represents a particular InterfaceID.
There are a number of standard shell interfaces that can be associated with a file type:
{BB2E617C-0920-11d1-9A0B-00C04FC2D6C1} IExtractImage
{953BB1EE-93B4-11d1-98A3-00C04FB687DA} IExtractImage2
{e357fccd-a995-4576-b01f-234630154e96} IThumbnailProvider
{8895b1c6-b41f-4c1c-a562-0d564250836f} IPreviewHandler
Unsupported spelunking of undocumented registry keys
If i want to find, for example, the clsid of the IPreviewHandler associated with a .jpg file, i would look in:
HKEY_CLASSES_ROOT/.jpg/ShellEx/{8895b1c6-b41f-4c1c-a562-0d564250836f}
(default) = [clsid]
But that's not the only place i could look. I can also look in:
HKEY_CLASSES_ROOT/.jpg
(default) = jpgfile
HKEY_CLASSES_ROOT/jpgfile/ShellEx/{8895b1c6-b41f-4c1c-a562-0d564250836f}
(default) = [clsid]
But that's not the only place i could look. I can also look in:
HKEY_CLASSES_ROOT/SystemFileAssociations/.jpg/ShellEx/{8895b1c6-b41f-4c1c-a562-0d564250836f}
(default) = [clsid]
But that's not the only place i could look. I can also look in:
HKEY_CLASSES_ROOT/SystemFileAssociations/jpegfile/ShellEx/{8895b1c6-b41f-4c1c-a562-0d564250836f}
(default) = [clsid]
But that's not the only place i could look. If i think the file is an image, i can also look in:
HKEY_CLASSES_ROOT/SystemFileAssociations/image/ShellEx/{8895b1c6-b41f-4c1c-a562-0d564250836f}
(default) = [clsid]
How did i find these locations? Did i only follow documented and supported locations? No, i spied on Explorer using Process Monitor as it went hunting for an IThumbnailProvider.
Don't use undocumented spellunking
So now i want to use a standard shell interface for a file-type myself. This means that i have to crawl the locations. But why crawl these locations in an undocumented, unsupported way. Why incur the wrath from the guy from high atop the thing? Use AssocQueryString:
Guid GetShellClsidForFileType(String fileExtension, Guid interfaceID)
{
//E.g.:
// String fileExtension = ".jpg"
// Guid interfaceID = "{8895b1c6-b41f-4c1c-a562-0d564250836f}"; //IExtractImage
//The interface we're after - in string form
String szInterfaceID := GuidToString(interfaceID);
//Buffer to receive the clsid string
DWORD bufferSize := 1024; //more than enough to hold a 38-character clsid
String buffer;
SetLength(buffer, bufferSize);
HRESULT hr := AssocQueryString(
ASSOCF_INIT_DEFAULTTOSTAR,
ASSOCSTR_SHELLEXTENSION, //for finding shell extensions
fileExtension, //e.g. ".txt"
szInterfaceID, //e.g. "{8895b1c6-b41f-4c1c-a562-0d564250836f}"
buffer, //will receive the clsid string
#bufferSize);
if (hr <> S_OK)
return Guid.Empty;
Guid clsid;
HRESULT hr = CLSIDFromString(buffer, out clsid);
if (hr <> NOERROR)
return Guid.Empty;
return clsid;
}
And so to get the clsid of IPreviewHandler for .xps files:
Guid clsid = GetShellClsidForFileType(".xps", IPreviewHandler);
How to get IPreviewHandler for a file extension?
With all the above, we can now answer the question:
IPreviewHandler GetPreviewHandlerForFileType(String extension)
{
//Extension: the file type to return IPreviewHandler for (e.g. ".xps")
Guid previewHandlerClassID = GetShellClsidForFileType(extension, IPreviewHandler);
//Create the COM object
IUnknown unk = CreateComObject(previewHandlerClassID);
//Return the actual IPreviewHanler interface (not IUnknown)
return (IPreviewhandler)unk;
}
I'm using Microsoft's DSOFramer control to allow me to embed an Excel file in my dialog so the user can choose his sheet, then select his range of cells; it's used with an import button on my dialog.
The problem is that when I call the DSOFramer's OPEN function, if I have Excel open in another window, it closes the Excel document (but leaves Excel running). If the document it tries to close has unsaved data, I get a dialog boxclosing Excel doc in another window. If unsaved data in file, dsoframer fails to open with a messagebox: Attempt to access invalid address.
I built the source, and stepped through, and its making a call in its CDsoDocObject::CreateFromFile function, calling BindToObject on an object of class IMoniker. The HR is 0x8001010a The message filter indicated that the application is busy. On that failure, it tries to InstantiateDocObjectServer by classid of CLSID Microsoft Excel Worksheet... this fails with an HRESULT of 0x80040154 Class not registered. The InstantiateDocObjectServer just calls CoCreateInstance on the classid, first with CLSCTX_LOCAL_SERVER, then (if that fails) with CLSCTX_INPROC_SERVER.
I know DSOFramer is a popular sample project for embedding Office apps in various dialog and forms. I'm hoping someone else has had this problem and might have some insight on how I can solve this. I really don't want it to close any other open Excel documents, and I really don't want it to error-out if it can't close the document due to unsaved data.
Update 1: I've tried changing the classid that's passed in to Excel.Application (I know that class will resolve), but that didn't work. In CDsoDocObject, it tries to open key HKEY_CLASSES_ROOT\CLSID\{00024500-0000-0000-C000-000000000046}\DocObject, but fails. I've visually confirmed that the key is not present in my registry; The key is present for the guide, but there's no DocObject subkey. It then produces an error message box: The associated COM server does not support ActiveX document embedding. I get similar (different key, of course) results when I try to use the Excel.Workbook programid.
Update 2: I tried starting a 2nd instance of Excel, hoping that my automation would bind to it (being the most recently invoked) instead of the problem Excel instance, but it didn't seem to do that. Results were the same. My problem seems to have boiled down to this: I'm calling the BindToObject on an object of class IMoniker, and receiving 0x8001010A (RPC_E_SERVERCALL_RETRYLATER) The message filter indicated that the application is busy. I've tried playing with the flags passed to the BindToObject (via the SetBindOptions), but nothing seems to make any difference.
Update 3: It first tries to bind using an IMoniker class. If that fails, it calls CoCreateInstance for the clsid as a fallback method. This may work for other MS Office objects, but when it's Excel, the class is for the Worksheet. I modified the sample to CoCreateInstance _Application, then got the workbooks, then called the Workbooks::Open for the target file, which returns a Worksheet object. I then returned that pointer and merged back with the original sample code path. All working now.
#Jinjin
You can use the #import directive to import your Excel's OLB file. this should generate (and automatically include an Excel .tlh file which contains the structures for _Application (and the rest you need)). Ideally, you should find an OLB file that matches the earliest Excel version that you wish to support. The one on your local system is probably in c:\Program Files\Microsoft Office\Office12 (presuming you have Office 2007 installed). It may be named Excel.olb, or XL5EN32.OLB (different, obviously if you haven't installed the US English verion of Excel.
So, copy the .olb file to your project source directory, then at the top of the source file, add a line for #import "XL5EN32.olb".
Yes, opens older versions. Best way to guarantee that this will be the case is to find an OLB file (mentioned in item 1 above) that is from an installation of Excel that is the earliest version you wish to support. I use an Excel9.olb from Office 2000. Works fine with my testing of Excel versions all the way to the latest from Office 2007.
Yes, you should use dsoframer normally after making these changes.
I'm afraid I probably can't do that due to restrictions of my employer. However, if you take the "stock" dsoframer project, make the changes described in part 1 of this post, and the changes I described in my earlier post, you have pretty much recreated exactly what I have.
#Jinjin: did you put the import statement (#import "XL5EN32.olb") in the cpp file where you are using the Excel::_Application? If not, do that... can't just add it to the project. If you have already done that, try also adding this statement to the cpp file where you are using those mappings #import "Debug\XL5EN32.tlh". The tlh file is a header that is generated by running the #import; you should find it in your Debug directory (presuming you're performing a Debug build).
Renaming _Application to Application (and the others) is not the right way to go. The _Application structure is the one that has the mappings. That is why you are not finding the app->get_Workbooks.
What file are you looking in that you are finding Application but not _Application?
Assuming you are using the DSOFRAMER project, you need to add this code to dsofdocobj.cpp in the CreateFromFile function, at around line 348:
CLSID clsidExcelWS;
hr = CLSIDFromProgID(OLESTR("Excel.Sheet"),clsidExcelWS);
if (FAILED(hr)) return hr;
if (clsid == clsidExcelWS)
{
hr = InstantiateAndLoadExcel(pwszFile, &pole);
if (FAILED(hr)) return hr;
}
else
{
<the IMoniker::BindToObject call and it's failure handling from the "stock" sample goes here>
}
Then, define the following new member function in CDsoDocObject:
////////////////////////////////////////////////////////////////////////
// CDsoDocObject::InstantiateAndLoadExcel (protected)
//
// Create an instance of Excel and load the target file into its worksheet
//
STDMETHODIMP CDsoDocObject::InstantiateAndLoadExcel(LPWSTR pwszFile, IOleObject **ppole)
{
IUnknown *punkApp=NULL;
Excel::_Application *app=NULL;
Excel::Workbooks *wbList=NULL;
Excel::_Workbook *wb;
CLSID clsidExcel;
HRESULT hr = CLSIDFromProgID(OLESTR("Excel.Application"), &clsidExcel);
if (FAILED(hr))
return hr;
hr = CoCreateInstance(clsidExcel, NULL, CLSCTX_LOCAL_SERVER, IID_IUnknown, (void**)&punkApp);
if (SUCCEEDED(hr))
{
hr = punkApp->QueryInterface(__uuidof(Excel::_Application),(LPVOID *)&app);
if (SUCCEEDED(hr))
{
hr = app->get_Workbooks(&wbList);
VARIANT vNoParam;
VariantInit(&vNoParam);
V_VT(&vNoParam) = VT_ERROR;
V_ERROR(&vNoParam) = DISP_E_PARAMNOTFOUND;
VARIANT vReadOnly;
VariantInit(&vReadOnly);
V_VT(&vReadOnly) = VT_BOOL;
V_BOOL(&vReadOnly) = VARIANT_TRUE;
BSTR bstrFilename = SysAllocString(pwszFile);
hr = wbList->Open(bstrFilename, vNoParam,vNoParam,vNoParam,vNoParam,vReadOnly,vNoParam,vNoParam,vNoParam,vNoParam,vNoParam,vNoParam,vNoParam,0,&wb);
if (SUCCEEDED(hr))
hr = wb->QueryInterface(IID_IOleObject, (void**)ppole);
VariantClear(&vReadOnly);
VariantClear(&vNoParam);
SysFreeString(bstrFilename);
}
}
if (wb != NULL) wb->Release();
if (wbList != NULL) wbList->Release();
if (app != NULL) app->Release();
if (punkApp != NULL) punkApp->Release();
return hr;
}