For a WPF WebBrowser control, is there a way to duplicate Internet Explorer's zoom functionality?
In other words, Internet Explorer has the menu View > Zoom > 75%, which renders the web page at 75% scale. Is there a way to make a web browser control, which is embedded in a WPF app, do the same thing?
I've seen this post:
WPF WebBrowser - How to Zoom Content?
But it only seems to scale the page and not the page content.
public partial class TestWindow: UserControl
{
public TestWindow()
{
InitializeComponent();
browser.LoadCompleted += new LoadCompletedEventHandler(browser_LoadCompleted);
}
private void browser_LoadCompleted(object sender, NavigationEventArgs e)
{
try
{
FieldInfo webBrowserInfo = browser.GetType().GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic);
object comWebBrowser = null;
object zoomPercent = 120;
if (webBrowserInfo != null)
comWebBrowser = webBrowserInfo.GetValue(browser);
if (comWebBrowser != null)
{
InternetExplorer ie = (InternetExplorer)comWebBrowser;
ie.ExecWB(SHDocVw.OLECMDID.OLECMDID_OPTICAL_ZOOM, SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER, ref zoomPercent, IntPtr.Zero);
}
}
catch (Exception ex)
{
}
}
public void SetBrowser(string url)
{
browser.Navigate(url,null,null,null);
}
internal void Destroy()
{
try
{
if (browser.Parent != null)
{
((Grid)browser.Parent).Children.Remove(browser);
browser.Navigate("about:blank");
browser.Dispose();
browser = null;
}
}
catch { }
}
}
Here's how I did it:
// Needed to expose the WebBrowser's underlying ActiveX control for zoom functionality
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
internal interface IServiceProvider
{
[return: MarshalAs(UnmanagedType.IUnknown)]
object QueryService(ref Guid guidService, ref Guid riid);
}
static readonly Guid SID_SWebBrowserApp = new Guid("0002DF05-0000-0000-C000-000000000046");
private void ZoomListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
object zoomPercent; // A VT_I4 percentage ranging from 10% to 1000%
switch(ZoomListBox.SelectedItem.ToString())
{
case "System.Windows.Controls.ListBoxItem: 200%":
zoomPercent = 200;
break;
case "System.Windows.Controls.ListBoxItem: 100%":
zoomPercent = 100;
break;
case "System.Windows.Controls.ListBoxItem: 50%":
zoomPercent = 50;
break;
default:
zoomPercent = 100;
break;
}
// grab a handle to the underlying ActiveX object
IServiceProvider serviceProvider = null;
if (m_webView.Document != null)
{
serviceProvider = (IServiceProvider)m_webView.Document;
}
Guid serviceGuid = SID_SWebBrowserApp;
Guid iid = typeof(SHDocVw.IWebBrowser2).GUID;
SHDocVw.IWebBrowser2 browserInst = (SHDocVw.IWebBrowser2)serviceProvider.QueryService(ref serviceGuid, ref iid);
// send the zoom command to the ActiveX object
browserInst.ExecWB(SHDocVw.OLECMDID.OLECMDID_OPTICAL_ZOOM, SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER, ref zoomPercent, IntPtr.Zero);
}
All the service provider stuff exposes the ActiveX since the WPF WebBrowser control doesn't expose it directly. Aside from that, it's pretty much the same as alexei's solution.
This is not an exact answer since it is for the WinForms control, but perhaps will be useful in case you decide to use it in a WindowsFormsHost instead of the WPF control, which exposes way too little to be useful.
You could use an OLE commands through ExecWB on the ActiveX instance: OLECMDID_ZOOM for text size and OLECMDID_OPTICAL_ZOOM for optical zoom. For example,
object pvaIn = 200; // A VT_I4 percentage ranging from 10% to 1000%
var browserInst = ((SHDocVw.IWebBrowser2)(browserContol.ActiveXInstance));
browserInst.ExecWB(SHDocVw.OLECMDID.OLECMDID_OPTICAL_ZOOM,
SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER,
ref pvaIn, IntPtr.Zero);
Some notes:
a reference to Interop.SHDocVw assembly is needed
the command succeeds only after a document has loaded
the range of pvaIn could be retrieved via OLECMDID_GETZOOMRANGE
for reference list of commands is on MSDN
I experienced this strange behavior that seemed to happen only on non-96 dpi. Upon startup, the rendered text size did not correspond to that stored in OLECMDID_ZOOM state. Setting the value (to any value) did not fix the discrepancy: the rendered size is still what looked like [stored size + 2]. When optical zoom was set to 100%, the discrepancy in text-size went away (text size visibly shrank after zooming to 100%). This did't happen in IE, and perhaps that was just a weird artifact in my environment -- but just fyi.
When using the other solutions, I always get errors of kind
HRESULT: 0x80040100
DRAGDROP_E_NOTREGISTERED
I found a solution on this page that worked for me:
var wb = webBrowser.ActiveXInstance.GetType();
object o = zoomPercentage; // Between 10 and 1000.
wb.InvokeMember(
#"ExecWB",
BindingFlags.InvokeMethod,
null,
webBrowser.ActiveXInstance,
new[]
{
OLECMDID.OLECMDID_OPTICAL_ZOOM,
OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER,
o,
o
});
With OLECMDID_OPTICAL_ZOOM being 63 and OLECMDEXECOPT_DONTPROMPTUSER being 2.
Related
i'm developing VSTO add-in for outlook which includes overlay on top of the window.
I'm building my UI using WPF.
Problem is that when i'm trying to attach WPF Window ( merge left/top/width/height ) to outlook window when STARTING at scale more than 100% GetWindowsRect Returns wrong rectangle.
BUT when i'm starting application at 100% scale then change windows scale at runtime to whatever value everything is good and DPI Aware. Both cases ( starting and runtime ) GetDpiForWindow returns correct values which is...strange. DPI Awareness is set using SetThreadDpiAwareness when forms are created.
Can't get my head what's wrong :<. Any advises appreciated.
Code for attaching:
public void AttachTo(IntPtr src, AttachFlagEnum flags)
{
var nativeRectangle = new WinAPI.RECT();
if (!WinAPI.GetWindowRect(src, ref nativeRectangle))
{
// throw new Win32Exception(Marshal.GetLastWin32Error());
return;
}
AttachToCoords(new Rectangle(nativeRectangle.Left, nativeRectangle.Top, nativeRectangle.Right - nativeRectangle.Left, nativeRectangle.Bottom - nativeRectangle.Top), flags);
}
Form create code:
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
StateManager.Init();
OutlookUtils.WaitOutlookLoading();
using (var ctx = new DPIContextBlock(WinAPI.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE))
{
new Forms.One().Show();
new Forms.Overlay().Show();
new Forms.Two();
}
}
Overlay attach code (executes by timer )
private void OverlayThink(object ob)
{
if (Managers.StateManager.OutlookState == OutlookStateEnum.MINIMIZED || Managers.StateManager.UiState == UIStateEnum.DESCWND)
{
if (this.IsVisible)
{
this.Dispatcher.Invoke(() => this.Hide());
}
return;
}
this.Dispatcher.Invoke(() => this.AttachTo(Utils.OutlookUtils.GetWordWindow(), AttachFlagEnum.OVERLAY));
this.Dispatcher.Invoke(() => this.Show());
}
My solution to the problem was that AttachToCoords method sets coords from GetWindowRect directly to Window.Left | Right | Top | Bottom. That's wrong because internally WPF positions it's elements in 96 DPI coordinate system. So i was need to convert it before assigning.
Solution:
private Rectangle TransformCoords(Rectangle coords)
{
var source = PresentationSource.FromVisual(this);
coords.X = (int)(coords.X / source.CompositionTarget.TransformToDevice.M11);
coords.Y = (int)(coords.Y / source.CompositionTarget.TransformToDevice.M22);
coords.Width = (int)(coords.Width / source.CompositionTarget.TransformToDevice.M11);
coords.Height = (int)(coords.Height / source.CompositionTarget.TransformToDevice.M22);
return coords;
}
WPF (as well as Windows forms) should be scaled automatically depending on the DPI value set on the system. There is no need to calculate the size and positions of the dialog window in Outlook add-ins.
Instead, you need to set up the form correctly to follow the DPI settings and set the window parent, so it will be displayed on top of the Outlook window.
I create a winform project with a single form with 4 textboxes and a button.
On button click, I perform the following:
Window1 w = new Window1();
ElementHost.EnableModelessKeyboardInterop(w);
w.Show();
Where window 1 is a Wpf window. Window1 has a single button on it and when that button is clicked the following occurs:
System.Windows.MessageBox.Show("HelloWOrld");
When you run the application the WinForm Form pops ups. If you hit tab it cycles through the 4 textboxes no problem. Then Click the button to open the WPF window. Click that button and popup the messagebox. Leave them open and then go back to the WinForm form you can no longer tab through the fields but you can type other characters. It appears as though the textboxes get the keystrokes but the form doesn't get them. I also get a system beep as though the model was getting the keystroke.
EDIT 9/9/2014 3:44PM
Hans responded in the comments and was correct. I tried describing a simpler case that would be easier for other people to reproduce that gave use the same symptoms. Our actual problem is that we have created a window base class that supports modal to parent capabilities. Here is the relevant code for our BaseWindow
public class BaseWindow: Window
{
[DllImport("user32.dll")]
static extern bool EnableWindow(IntPtr hWnd, bool bEnable);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);
public void ShowModalToParent(Window frmParent, Action<bool?> callback = null)
{
IntPtr myHandle = (new System.Windows.Interop.WindowInteropHelper(this)).Handle;
EnableWindow(myHandle,
SetForegroundWindow(myHandle);
this.Closing += Window_Closing;
ShowInTaskbar = false;
Owner = frmParent; // Keep on top of parent
ClosedCallBack += callback ?? (p => { _modalDialogResult = p; });
var parentHandle = (new System.Windows.Interop.WindowInteropHelper(frmParent)).Handle;
EnableWindow(parentHandle, false); // Prevent events for parent
new ShowAndWaitHelper(this).ShowAndWait();
}
internal class ShowAndWaitHelper
{
private readonly Window _window;
private DispatcherFrame _dispatcherFrame;
internal ShowAndWaitHelper(Window window)
{
if (window == null)
{
throw new ArgumentNullException("panel");
}
this._window = window;
}
internal void ShowAndWait()
{
if (this._dispatcherFrame != null)
{
throw new InvalidOperationException("Cannot call ShowAndWait while waiting for a previous call to ShowAndWait to return.");
}
this._window.Closed += new EventHandler(this.OnPanelClosed);
_window.Show();
this._dispatcherFrame = new DispatcherFrame();
Dispatcher.PushFrame(this._dispatcherFrame);
}
private void OnPanelClosed(object source, EventArgs eventArgs)
{
if (this._dispatcherFrame == null)
{
return;
}
this._window.Closed -= new EventHandler(this.OnPanelClosed);
this._dispatcherFrame.Continue = false;
this._dispatcherFrame = null;
}
}
}
I'm sure this code was taken from a Blog/Forum post of some sort but am unable to find any reference to it in code. We want to keep the modal to parent but some how address the odd key press issue. To reproduce the issue replace the button_click in Window1 to call ShowModalToParent on a window that uses this as a base class.
I have a barebones WPF app that has about a Meg of ASCII text to display. I initially put a TextBlock in a WrapPanel in a ScrollViewer. This correctly scrolled and resized when I resized the window, but it was super slow! I needed something faster.
So I put the text in FormattedText, and rendered that using a custom control. That was much faster, but it didn't resize. So I made my custom control resize. But it would ReDraw too many times a second, so I made it only redraw every 100ms.
Much better. Rendering and Resizing still isn't great but it's much better than it was. But I lost scrolling.
Eventually I need a solution that does a lot - but for now I'm trying to have a solution that does a little: show a mem of text, wrap, have a scrollbar, and be performant. Eventually, I'd like it to scale to a gig of text, have colors inline, some mouseover/click events for portions of the text...
How can I make FormattedText (or perhaps more accurately, a DrawingVisual) have a Vertical Scrollbar?
Here's my FrameworkElement that shows my FormattedText:
using System;
using System.Windows;
using System.Windows.Media;
namespace Recall
{
public class LightweightTextBox : FrameworkElement
{
private VisualCollection _children;
private FormattedText _formattedText;
private System.Threading.Timer _resizeTimer;
private const int _resizeDelay = 100;
public double MaxTextWidth
{
get { return this._formattedText.MaxTextWidth; }
set { this._formattedText.MaxTextWidth = value; }
}
public LightweightTextBox(FormattedText formattedText)
{
this._children = new VisualCollection(this);
this._formattedText = formattedText;
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawText(this._formattedText, new Point(0, 0));
drawingContext.Close();
_children.Add(drawingVisual);
this.SizeChanged += new SizeChangedEventHandler(LightweightTextBox_SizeChanged);
}
void LightweightTextBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
this.MaxTextWidth = e.NewSize.Width;
if (_resizeTimer != null)
_resizeTimer.Change(_resizeDelay, System.Threading.Timeout.Infinite);
else
_resizeTimer = new System.Threading.Timer(new System.Threading.TimerCallback(delegate(object state)
{
ReDraw();
if (_resizeTimer == null) return;
_resizeTimer.Dispose();
_resizeTimer = null;
}), null, _resizeDelay, System.Threading.Timeout.Infinite);
}
public void ReDraw()
{
this.Dispatcher.Invoke((Action)(() =>
{
var dv = _children[0] as DrawingVisual;
DrawingContext drawingContext = dv.RenderOpen();
drawingContext.DrawText(this._formattedText, new Point(0, 0));
drawingContext.Close();
}));
}
//===========================================================
//Overrides
protected override int VisualChildrenCount { get { return _children.Count; } }
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _children.Count)
throw new ArgumentOutOfRangeException();
return _children[index];
}
}
}
For simple text a readonly TextBox is pretty good. For more complex matters you can use FlowDocuments (which can be hosted in a FlowDocumentScrollViewer), TextBlocks also host flow content but are not intended for larger amounts.
MSDN:
TextBlock is not optimized for scenarios that need to display more than a few lines of content; for such scenarios, a FlowDocument coupled with an appropriate viewing control is a better choice than TextBlock, in terms of performance. After TextBlock, FlowDocumentScrollViewer is the next lightest-weight control for displaying flow content, and simply provides a scrolling content area with minimal UI. FlowDocumentPageViewer is optimized around "page-at-a-time" viewing mode for flow content. Finally, FlowDocumentReader supports the richest set functionality for viewing flow content, but is correspondingly heavier-weight.
I have a WPF application that uses the WPF WebBrowser control to display interesting web pages to our developers on a flatscreen display (like a news feed).
The trouble is that I occasionally get a HTML script error that pops up a nasty IE error message asking if I would like to "stop running scripts on this page". Is there a way to suppress this error checking?
NOTE: I have disabled script debugging in IE settings already.
Here is a solution i just made with reflection. Solves the issue :)
I run it at the Navigated event, as it seems the activeX object is not available until then.
What it does is set the .Silent property on the underlying activeX object. Which is the same as the .ScriptErrorsSuppressed property which is the Windows forms equivalent.
public void HideScriptErrors(WebBrowser wb, bool Hide) {
FieldInfo fiComWebBrowser = typeof(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic);
if (fiComWebBrowser == null) return;
object objComWebBrowser = fiComWebBrowser.GetValue(wb);
if (objComWebBrowser == null) return;
objComWebBrowser.GetType().InvokeMember("Silent", BindingFlags.SetProperty, null, objComWebBrowser, new object[] { Hide });
}
A better version that can be run anytime and not after the .Navigated event:
public void HideScriptErrors(WebBrowser wb, bool hide) {
var fiComWebBrowser = typeof(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic);
if (fiComWebBrowser == null) return;
var objComWebBrowser = fiComWebBrowser.GetValue(wb);
if (objComWebBrowser == null) {
wb.Loaded += (o, s) => HideScriptErrors(wb, hide); //In case we are to early
return;
}
objComWebBrowser.GetType().InvokeMember("Silent", BindingFlags.SetProperty, null, objComWebBrowser, new object[] { hide });
}
If any issues with the second sample, try swapping wb.Loaded with wb.Navigated.
Just found from another question, this is elegant and works great.
dynamic activeX = this.webBrowser1.GetType().InvokeMember("ActiveXInstance",
BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
null, this.webBrowser1, new object[] { });
activeX.Silent = true;
The problem here is that the WPF WebBrowser did not implement this property as in the 2.0 control.
Your best bet is to use a WindowsFormsHost in your WPF application and use the 2.0's WebBrowser property: SuppressScriptErrors. Even then, you will need the application to be full trust in order to do this.
Not what one would call ideal, but it's pretty much the only option currently.
I've also found an interesting way to disable JavaScript errors. But you need to use at least .Net Framework 4.0 because of using elegant dynamic type.
You need to subscribe to the LoadCompleted event of the WebBrowser element:
<WebBrowser x:Name="Browser"
LoadCompleted="Browser_OnLoadCompleted" />
After that you need to write an event handler that looks like below:
void Browser_OnLoadCompleted(object sender, NavigationEventArgs e)
{
var browser = sender as WebBrowser;
if (browser == null || browser.Document == null)
return;
dynamic document = browser.Document;
if (document.readyState != "complete")
return;
dynamic script = document.createElement("script");
script.type = #"text/javascript";
script.text = #"window.onerror = function(msg,url,line){return true;}";
document.head.appendChild(script);
}
I wanted to add this as a comment to #Alkampfer answer, but I don't have enough reputation. This works for me (Windows 8.1, NET 4.5):
window.Browser.LoadCompleted.Add(fun _ ->
window.Browser.Source <- new System.Uri("javascript:window.onerror=function(msg,url,line){return true;};void(0);"))
This code sample is written in F#, but it's pretty clear what it does.
Check the below code for suppressing script errors for WPF browser control..
public MainWindow
{
InitializeComponent();
WebBrowserControlView.Navigate(new Uri("https://www.hotmail.com"));
//The below checks for script errors.
ViewerWebBrowserControlView.Navigated += ViewerWebBrowserControlView_Navigated;
}
void ViewerWebBrowserControlView_Navigated(object sender, NavigationEventArgs e)
{
BrowserHandler.SetSilent(ViewerWebBrowserControlView, true); // make it silent
}
public static class BrowserHandler
{
private const string IWebBrowserAppGUID = "0002DF05-0000-0000-C000-000000000046";
private const string IWebBrowser2GUID = "D30C1661-CDAF-11d0-8A3E-00C04FC9E26E";
public static void SetSilent(System.Windows.Controls.WebBrowser browser, bool silent)
{
if (browser == null)
MessageBox.Show("No Internet Connection");
// get an IWebBrowser2 from the document
IOleServiceProvider sp = browser.Document as IOleServiceProvider;
if (sp != null)
{
Guid IID_IWebBrowserApp = new Guid(IWebBrowserAppGUID);
Guid IID_IWebBrowser2 = new Guid(IWebBrowser2GUID);
object webBrowser;
sp.QueryService(ref IID_IWebBrowserApp, ref IID_IWebBrowser2, out webBrowser);
if (webBrowser != null)
{
webBrowser.GetType().InvokeMember("Silent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.PutDispProperty, null, webBrowser, new object[] { silent });
}
}
}
}
[ComImport, Guid("6D5140C1-7436-11CE-8034-00AA006009FA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOleServiceProvider
{
[PreserveSig]
int QueryService([In] ref Guid guidService, [In] ref Guid riid, [MarshalAs(UnmanagedType.IDispatch)] out object ppvObject);
}
Whereas, If you are using Winforms Web browser with winforms host.. you have a property "SuppressScriptErrors" set it to true
<WindowsFormsHost Name="WinformsHost" Grid.Row="1">
<winForms:WebBrowser x:Name="WebBrowserControlView" ScriptErrorsSuppressed="True" AllowWebBrowserDrop="False"></winForms:WebBrowser>
</WindowsFormsHost>
I've this problem in the past and finally resolved it with an injection of a Javascript script that suppress error handling. Hope this could help you too.
Disable Javascript errors in WEbBrowsercontrol
you can use this trick
vb.net
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
Private Const WM_CLOSE As Short = &H10s
and call last lib :
dim hwnd
dim vreturnvalue
hwnd = FindWindow(vbNullString,"script error")
if hwnd<>0 then vreturnvalue = SendMessage(hwnd, WM_CLOSE, &O0s, &O0s)
Wolf5's answer is good but if (objComWebBrowser == null) doesn't seem to work. Instead I check the WebBrowser instance for IsLoaded:
public void HideScriptErrors(WebBrowser webBrowser)
{
if (!webBrowser.IsLoaded)
{
webBrowser.Loaded += WebBrowser_Loaded; // in case we are too early
return;
}
var objComWebBrowser = typeof(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(webBrowser);
if (objComWebBrowser == null)
{
return;
}
objComWebBrowser.GetType().InvokeMember("Silent", BindingFlags.SetProperty, null, objComWebBrowser, new object[] { true });
}
private void WebBrowser_Loaded(object sender, RoutedEventArgs e)
{
var webBrowser = sender as WebBrowser;
webBrowser.Loaded -= WebBrowser_Loaded;
HideScriptErrors(webBrowser);
}
It is also necessary to remove the Loaded event handler after the first time, as the control may be unloaded and loaded again several times when making it invisible by switching to a different tab. Also the if (!wb.Loaded)) fallback is still important when the WebBrowser is not visible yet on its first navigation, e.g. if it is on a different tab that is not visible on application startup.
For a System.Windows.Forms.TextBox with Multiline=True, I'd like to only show the scrollbars when the text doesn't fit.
This is a readonly textbox used only for display. It's a TextBox so that users can copy the text out. Is there anything built-in to support auto show of scrollbars? If not, should I be using a different control? Or do I need to hook TextChanged and manually check for overflow (if so, how to tell if the text fits?)
Not having any luck with various combinations of WordWrap and Scrollbars settings. I'd like to have no scrollbars initially and have each appear dynamically only if the text doesn't fit in the given direction.
#nobugz, thanks, that works when WordWrap is disabled. I'd prefer not to disable wordwrap, but it's the lesser of two evils.
#André Neves, good point, and I would go that way if it was user-editable. I agree that consistency is the cardinal rule for UI intuitiveness.
I came across this question when I wanted to solve the same problem.
The easiest way to do it is to change to System.Windows.Forms.RichTextBox. The ScrollBars property in this case can be left to the default value of RichTextBoxScrollBars.Both, which indicates "Display both a horizontal and a vertical scroll bar when needed." It would be nice if this functionality were provided on TextBox.
Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto your form. It's not quite perfect but ought to work for you.
using System;
using System.Drawing;
using System.Windows.Forms;
public class MyTextBox : TextBox {
private bool mScrollbars;
public MyTextBox() {
this.Multiline = true;
this.ReadOnly = true;
}
private void checkForScrollbars() {
bool scroll = false;
int cnt = this.Lines.Length;
if (cnt > 1) {
int pos0 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(0)).Y;
if (pos0 >= 32768) pos0 -= 65536;
int pos1 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(1)).Y;
if (pos1 >= 32768) pos1 -= 65536;
int h = pos1 - pos0;
scroll = cnt * h > (this.ClientSize.Height - 6); // 6 = padding
}
if (scroll != mScrollbars) {
mScrollbars = scroll;
this.ScrollBars = scroll ? ScrollBars.Vertical : ScrollBars.None;
}
}
protected override void OnTextChanged(EventArgs e) {
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnClientSizeChanged(EventArgs e) {
checkForScrollbars();
base.OnClientSizeChanged(e);
}
}
I also made some experiments, and found that the vertical bar will always show if you enable it, and the horizontal bar always shows as long as it's enabled and WordWrap == false.
I think you're not going to get exactly what you want here. However, I believe that users would like better Windows' default behavior than the one you're trying to force. If I were using your app, I probably would be bothered if my textbox real-estate suddenly shrinked just because it needs to accomodate an unexpected scrollbar because I gave it too much text!
Perhaps it would be a good idea just to let your application follow Windows' look and feel.
There's an extremely subtle bug in nobugz's solution that results in a heap corruption, but only if you're using AppendText() to update the TextBox.
Setting the ScrollBars property from OnTextChanged will cause the Win32 window (handle) to be destroyed and recreated. But OnTextChanged is called from the bowels of the Win32 edit control (EditML_InsertText), which immediately thereafter expects the internal state of that Win32 edit control to be unchanged. Unfortunately, since the window is recreated, that internal state has been freed by the OS, resulting in an access violation.
So the moral of the story is: don't use AppendText() if you're going to use nobugz's solution.
I had some success with the code below.
public partial class MyTextBox : TextBox
{
private bool mShowScrollBar = false;
public MyTextBox()
{
InitializeComponent();
checkForScrollbars();
}
private void checkForScrollbars()
{
bool showScrollBar = false;
int padding = (this.BorderStyle == BorderStyle.Fixed3D) ? 14 : 10;
using (Graphics g = this.CreateGraphics())
{
// Calcualte the size of the text area.
SizeF textArea = g.MeasureString(this.Text,
this.Font,
this.Bounds.Width - padding);
if (this.Text.EndsWith(Environment.NewLine))
{
// Include the height of a trailing new line in the height calculation
textArea.Height += g.MeasureString("A", this.Font).Height;
}
// Show the vertical ScrollBar if the text area
// is taller than the control.
showScrollBar = (Math.Ceiling(textArea.Height) >= (this.Bounds.Height - padding));
if (showScrollBar != mShowScrollBar)
{
mShowScrollBar = showScrollBar;
this.ScrollBars = showScrollBar ? ScrollBars.Vertical : ScrollBars.None;
}
}
}
protected override void OnTextChanged(EventArgs e)
{
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnResize(EventArgs e)
{
checkForScrollbars();
base.OnResize(e);
}
}
What Aidan describes is almost exactly the UI scenario I am facing. As the text box is read only, I don't need it to respond to TextChanged. And I'd prefer the auto-scroll recalculation to be delayed so it's not firing dozens of times per second while a window is being resized.
For most UIs, text boxes with both vertical and horizontal scroll bars are, well, evil, so I'm only interested in vertical scroll bars here.
I also found that MeasureString produced a height that was actually bigger than what was required. Using the text box's PreferredHeight with no border as the line height gives a better result.
The following seems to work pretty well, with or without a border, and it works with WordWrap on.
Simply call AutoScrollVertically() when you need it, and optionally specify recalculateOnResize.
public class TextBoxAutoScroll : TextBox
{
public void AutoScrollVertically(bool recalculateOnResize = false)
{
SuspendLayout();
if (recalculateOnResize)
{
Resize -= OnResize;
Resize += OnResize;
}
float linesHeight = 0;
var borderStyle = BorderStyle;
BorderStyle = BorderStyle.None;
int textHeight = PreferredHeight;
try
{
using (var graphics = CreateGraphics())
{
foreach (var text in Lines)
{
var textArea = graphics.MeasureString(text, Font);
if (textArea.Width < Width)
linesHeight += textHeight;
else
{
var numLines = (float)Math.Ceiling(textArea.Width / Width);
linesHeight += textHeight * numLines;
}
}
}
if (linesHeight > Height)
ScrollBars = ScrollBars.Vertical;
else
ScrollBars = ScrollBars.None;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
finally
{
BorderStyle = borderStyle;
ResumeLayout();
}
}
private void OnResize(object sender, EventArgs e)
{
m_timerResize.Stop();
m_timerResize.Tick -= OnDelayedResize;
m_timerResize.Tick += OnDelayedResize;
m_timerResize.Interval = 475;
m_timerResize.Start();
}
Timer m_timerResize = new Timer();
private void OnDelayedResize(object sender, EventArgs e)
{
m_timerResize.Stop();
Resize -= OnResize;
AutoScrollVertically();
Resize += OnResize;
}
}