Thank you all for the quick replies and suggestions.
My reasons for wanting to alter the appearance of cells (momentarily
or permanently) is that I have an RTD server and I want to be able to
highlight changes to cell values (e.g. set the cell background to red
if the value shifts by a certain % down or green if it shifts some %
upwards).
It is a shame that I can't do this directly through the C-API as it
would have been nice to be able to bypass the painfully slow COM API.
(I saw that the ManagedXLL library demonstrates behavior like this on
their feature page
http://www.stochastix.de/solutions/excel/managedxll/latest/features,
"Worksheet functions" does that sound like COM usage?)
Anyways, I tried two approaches to solviing this issue using a rather
simplistic RTD server that every two seconds updated all topics with a
random number:
1)
Implementing a rather naive (and excruciatingly slow) method of
setting the colorindex of the cell.
[ExcelCommand( Description = "Sets the background color of a cell to a
certain value (macro)" )]
public static void SetCellColorIndex( ExcelReference reference, int
colorIndex )
{
string r1 = Utilities.GetExcelCellReference
( reference.ColumnFirst, reference.RowFirst );
string r2 = Utilities.GetExcelCellReference( reference.ColumnLast,
reference.RowLast );
// LogDisplay.WriteLine( "Setting COLOR REF, " + r1 + "," + r2 + "
- " + colorIndex );
try
{
Instance.xlApp.get_Range( r1, r2 ).Interior.ColorIndex =
colorIndex;
}
catch( Exception ex )
{
//MessageBox.Show( ex.Message );
}
}
This approach I can't recommend as it is making to many expensive
calls over COM (assuming you update coloring every time the RTD
notifies of an update)
2)
Manipulating the cell's conditional formatting fields (as suggested by
Govert).
This approach worked out much better as instead of manually updating
the value every time the RTD server pushed out new values I could
simply set the conditional formatting once (when the RTD call is
created) and then have Excel handle all the messy stuff. A Timer class
handles all the COM stuff in the background. Bear with me, the code is
experimental and only intended to help people along (or spark a
discussion on what could be done better).
private struct ConditionalRange
{
public readonly ExcelReference reference;
public readonly double value;
public ConditionalRange( ExcelReference reference, double value )
{ this.reference = reference; this.value = value; }
}
private Timer mConditionalTimer = new Timer( ConditionalTimerCallback,
null, Timeout.Infinite, Timeout.Infinite );
private readonly OrderedDictionary<string, bool> mConditionalRanges =
new OrderedDictionary<string, bool>();
private readonly List<ConditionalRange> mConditionalRangesList = new
List<ConditionalRange>();
public void AddToConditionalRange( ExcelReference r, double value )
{
lock( mConditionalRangesList )
{
if( mConditionalTimerCanStart )
{
mConditionalTimer.Change( 2000, 1000 );
mConditionalTimerCanStart = false;
}
string r1 = Utilities.GetExcelCellReference(r.ColumnFirst,
r.RowFirst);
string r2 = Utilities.GetExcelCellReference(r.ColumnLast,
r.RowLast);
string range = r1 + ":" + r2;
if( !mConditionalRanges.ContainsKey( range ) )
{
mConditionalRangesList.Add(new ConditionalRange(new
ExcelReference(r.RowFirst, r.RowLast, r.ColumnFirst, r.ColumnLast),
value));
mConditionalRanges.Add( range, true );
}
}
}
public void RemoveConditionalRange(ExcelReference r)
{
lock (mConditionalRangesList)
{
string r1 = Utilities.GetExcelCellReference(r.ColumnFirst,
r.RowFirst);
string r2 = Utilities.GetExcelCellReference(r.ColumnLast,
r.RowLast);
string range = r1 + ":" + r2;
if (!mConditionalRanges.ContainsKey(range))
{
mConditionalRanges.Remove( range );
// TODO Delete also the range from the conditional
formatting list if it has yet to be applied
}
}
}
private void ConditionalTimerCallback( object state )
{
lock( mConditionalRangesList )
{
bool allok = true;
try
{
if( mConditionalRangesList.Count <= 0 )
return;
for( int i = 0; i < Math.Min( 20,
mConditionalRangesList.Count ); i++ )
{
try
{
ConditionalRange cRange = mConditionalRangesList
[i];
string r1 = Utilities.GetExcelCellReference
( cRange.reference.ColumnFirst, cRange.reference.RowFirst );
string r2 = r1;// Utilities.GetExcelCellReference
(cRange.reference.ColumnLast, cRange.reference.RowLast);
Instance.xlApp.get_Range( r1,
r2 ).FormatConditions.Delete();
IconSetCondition iconCond = (IconSetCondition)
Instance.xlApp.get_Range( r1, r2 ).FormatConditions.AddIconSetCondition
();
iconCond.IconSet = xlApp.ActiveWorkbook.IconSets
[XlIconSet.xl3Arrows];
iconCond.ModifyAppliesToRange(((Worksheet)
Instance.xlApp.ActiveSheet).get_Range(r1, r2).Cells);
/*
* Excel iterrates through the criterias in reverse
direction, and apparently the first item in the list
* nr = 1, is read-only and thus cannot be modified
*
http://social.msdn.microsoft.com/forums/en-US/vsto/thread/f9d6e6e3-a7e8-4237-a11d-a3b55c4a6c57/
* Also must set the type before you set the value,
because setting the type clears the value field!!!
*/
for( int j = 1; j < iconCond.IconCriteria.Count; j+
+ )
{
iconCond.IconCriteria[j + 1].Type =
XlConditionValueTypes.xlConditionValueNumber;
switch( j )
{
case 1:
iconCond.IconCriteria[j + 1].Operator
= (int)XlFormatConditionOperator.xlGreaterEqual; // Yellow condition
iconCond.IconCriteria[j + 1].Value =
cRange.value * 0.75;
break;
case 2:
iconCond.IconCriteria[j + 1].Operator
= (int)XlFormatConditionOperator.xlGreater; // Green condition
iconCond.IconCriteria[j + 1].Value =
cRange.value * 1.25;
break;
}
}
string rangeStr = Utilities.GetExcelCellReference
( cRange.reference.ColumnFirst, cRange.reference.RowFirst ) + ":" +
Utilities.GetExcelCellReference( cRange.reference.ColumnLast,
cRange.reference.RowLast );
if( mConditionalRanges.ContainsKey( rangeStr ) )
mConditionalRanges.Remove( rangeStr );
mConditionalRangesList.RemoveAt( i );
}
catch( Exception ex )
{
allok = false;
//mLogger.Error( "ERROR Adding condition " +
ex.Message );
}
}
if( allok )
allok = mConditionalRangesList.Count == 0;
}
catch( Exception ex )
{
allok = false;
//mLogger.Fatal( "ConditionalTimerCallback " + ex );
}
if( allok )
{
mConditionalTimer.Change( Timeout.Infinite,
Timeout.Infinite );
mConditionalTimerCanStart = true;
}
}
}
My RTD wrapper function (saves the calling cell and passes it into the
RTD server):
==============================
[ExcelFunction( Name = "RandomRTDFunction", Description = "Just a
simple random RTD function to test", Category = "Testing
ExcelDna",IsMacroType = true )]
public static object RandomRTDFunction( object feedcode, object
market )
{
try
{
if( !IsValid( feedcode ) || !IsValid( market ) )
return 0;
ExcelReference reference = Utilities.Caller();
LogDisplay.WriteLine( ( reference != null ? ( "Cell Ref:" +
reference.RowFirst + ":" + reference.ColumnFirst ) : "No
reference" ) );
object retValue = Instance.xlApp.WorksheetFunction.RTD
( "ExcelDnaRtdServer.SimpleRtdServer", "", "Func1",
(reference != null ? (reference.RowFirst + ":" +
reference.RowLast + ":" + reference.ColumnFirst + ":" +
reference.ColumnLast) : string.Empty),
feedcode, market,
MISSING, MISSING, MISSING, MISSING, MISSING, MISSING, MISSING,
MISSING, MISSING, MISSING, MISSING, MISSING, MISSING,MISSING, MISSING,
MISSING, MISSING, MISSING, MISSING, MISSING, MISSING, MISSING,
MISSING, MISSING );
return retValue;
}
catch( Exception ex )
{
return ex.Message;
}
}
== And the Utilities function to convert numerical values into Excel
Colum/Row references
/// <summary>
/// Expects a zero based indices to a row/column (adjust to
fit the excel way of referencing)
/// </summary>
/// <param name="column"></param>
/// <param name="row"></param>
/// <returns></returns>
[ExcelFunction(IsHidden = true)]
public static string GetExcelCellReference( int column, int
row)
{
return string.Format( "{0}{1}", ConvertToExcelColumnString
( column+1), row+1 );
}
private static string ConvertToExcelColumnString( int
remainder )
{
// Base case if empty
if (remainder <= 0)
return string.Empty;
string ret = string.Empty;
double div = remainder;
int mod = 0;
while( div > 26 )
{
mod = (int)( div % 26 );
if( mod == 0 )
{
mod = 26;
div = div - 1;
}
ret = (char)( 64 + mod ) + ret;
div /= 26;
}
ret = (char)( 64 + div ) + ret;
return ret;
}
Although the "registration" phase, that is the time it takes the
background thread to set all the conditional formulas takes a while to
complete this experiment works alright. However what needs to be done
is to throttle the RTD updates during the COM registration so that we
actually get access to the worksheet through COM (dirty i know). But a
start right?
What do the specialist think?
Could something like this be made faster by incorporating support for
it into the C wrapper in some way?
Cheers,
Sverrir S.
On Mar 8, 9:37 pm, incred <
grleach...@yahoo.com> wrote:
> Just as a query, what is your reason for wanting to do this?
>
> Sometimes it's better to let excel do what it's good at. This would be
> something that would be done using either Cell formating directly or
> conditionalformattingif you had multiple colours for multiple
> conditions.
>
> I would definitely suggest leaving your number crunching in your
> addin, and your presentation in your excel sheet/template.
>
> On Mar 6, 9:25 pm, Rookie <
r98...@hotmail.com> wrote:
>
> > You may try the following:
> > pass the worksheet object (say ws) and then declare the cell as a
> > range and use get_Range to format the cell e.g.
> > ws.get_Range("F8","Z1000").NumberFormat = "#,##0_);[Red](#,##0)";
>
> > On Mar 6, 4:20 am, Govert van Drimmelen <
gov...@icon.co.za> wrote:
>
> > > Hi,
>
> > > Excel does not allow you to make changes to the cellformattingas
> > > part of a function call. You'll be able to do this in a macro.
>
> > > Regards,
> > > Govert
>
> > > On Mar 4, 6:40 pm, sverrirs <
sverr...@gmail.com> wrote:
>
> > > > Hi,
>
> > > > First of all very impressed by the ExcelDna Library, excellent work.
>
> > > > And now for my newbie question:
> > > > Is it possible to change the background color / border / font
> > > >formattingof a cell through ExcelDna?