I know that there are several questions around the theme of page source but they don't seem related to my issue. I have also asked this question on the WebView2 GitHub website but it was closed.
Goal
I have an embedded WebView2 control on my CDialog and I am implementing a custom menu. I want to be able to view the page source and it should show in a popup. In-fact, it should show exactly as when you press CTRL + U in the browser control:
Issue
I added the following custom menu event handler to display the page source:
// ===============================================================
wil::com_ptr<ICoreWebView2ContextMenuItem> itemViewPageSource;
CHECK_FAILURE(webviewEnvironment->CreateContextMenuItem(
L"View Page Source (CTRL + U)", nullptr,
COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND, &itemViewPageSource));
CHECK_FAILURE(itemViewPageSource->add_CustomItemSelected(
Callback<ICoreWebView2CustomItemSelectedEventHandler>(
[appWindow = this, target](ICoreWebView2ContextMenuItem* sender, IUnknown* args)
{
wil::unique_cotaskmem_string pageUri;
CHECK_FAILURE(target->get_PageUri(&pageUri));
CString strUrl = L"view-source:" + CString(pageUri.get());
appWindow->NavigateTo(strUrl);
//appWindow->m_pImpl->m_webView->ExecuteScript(L"window.open(\"" + CString(pageUri.get()) + L"\", \"\", \"width=300, height=300\")", nullptr);
return S_OK;
})
.Get(), nullptr));
CHECK_FAILURE(items->InsertValueAtIndex(itemsCount, itemViewPageSource.get()));
itemsCount++;
// ===============================================================
The problem is that the source is not displayed in a new popup window (like when using the hotkey):
Update 1
I was able to change the code to use some JavaScript to display the page itself in a new window:
appWindow->m_pImpl->m_webView->ExecuteScript(L"window.open(\"" + CString(pageUri.get()) + L"\", \"\", \"width=300, height=300\")", nullptr);
And then, when I tried CTRL + U on the popup window, it appeared to display the source in the same window. But in actual fact it was a new window, as I could move it:
At this time I have not found out how to display the page source in a popup-up window (given teh context of my browser control) just like when you press CTRL + U.
The easiest way to do this is why the SendInput API:
CHECK_FAILURE(itemViewPageSource->add_CustomItemSelected(
Callback<ICoreWebView2CustomItemSelectedEventHandler>(
[appWindow = this, target](ICoreWebView2ContextMenuItem* sender, IUnknown* args)
{
wil::unique_cotaskmem_string pageUri;
CHECK_FAILURE(target->get_PageUri(&pageUri));
CString strUrl = L"view-source:" + CString(pageUri.get());
// Create an array of generic keyboard INPUT structures
INPUT ip[4] = {};
for (int n = 0; n < 4; ++n)
{
ip[n].type = INPUT_KEYBOARD;
ip[n].ki.wScan = 0;
ip[n].ki.time = 0;
ip[n].ki.dwFlags = 0; // 0 for key press
ip[n].ki.dwExtraInfo = 0;
}
ip[0].ki.wVk = VK_CONTROL;
ip[1].ki.wVk = 'U';
ip[2].ki.wVk = 'U';
ip[2].ki.dwFlags = KEYEVENTF_KEYUP;
ip[3].ki.wVk = VK_CONTROL;
ip[3].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(4, ip, sizeof(INPUT));
return S_OK;
})
.Get(), nullptr));
CHECK_FAILURE(items->InsertValueAtIndex(itemsCount, itemViewPageSource.get()));
itemsCount++;
Related
I’ve run into a performance obstacle and I’m uncertain of the cause, all of this is running under VS2022 & Net6.0. As this is my 1st time using this combination of a modal windows form, and progress bar, with the work running on a background thread and two Progress objects updating the UI, the progress bar, and a text label, I don’t know where to attack the problem. Prior to placing the workload on a background thread, everything was snappy, searching a thousand files with about 600 lines of text in each, in about a minute. Naturally, the windows form was frozen during this, which is why the workload was placed on a background thread.
After doing so, the workload will be 25-50% complete before the UI starts displaying the values from the Progress objects, and overall, the entire process now takes 10x as long to complete. Progress objects aren’t skipping over any values sent to them, the UI thread just seems slow in getting the information. Likewise, if I try to drag the modal form to a new spot on the desktop it’s unresponsive for 20—30 seconds before it finally moves. One more thing, I can step through the code on the background thread and see it calling the Progress updaters, but the UI thread is just very slow in responding to them.
I could use some suggestions on how to uncover the problem or if clearly evident, point out where the likely problem could be. Here are the essential controls and methods used.
public class SearchProgressForm : Form
{
private System.Windows.Forms.Button btnSearch = new Button();
private System.Windows.Forms.TextBox txtTextSearch = new TextBox();
private System.Windows.Forms.Label lblSearchFile = new Label();
private System.Windows.Forms.ProgressBar SearchProgressBar = new ProgressBar();
public event LogSearchEventHandler SearchSucceededEvent;
protected void OnSearchSucceeded(LogSearchEventArguments p_eventArguments)
{
LogSearchEventHandler handler = SearchSucceededEvent;
if (handler != null)
{
handler(this, p_eventArguments);
}
}
private void InitializeComponent()
{
this.btnSearch.Name = "btnSearch";
this.btnSearch.Text = "Search";
this.btnSearch.Click += new System.EventHandler(this.btnSearch_Click);
this.lblSearchFile.Text = "Searching File: ";
this.txtTextSearch.Text = "search string";
}
public SearchProgressForm() { }
private void btnSearch_Click(object sender, EventArgs e)
{
this.SearchByText(this.txtTextSearch.Text);
}
private void SearchByText(string p_searchParameter)
{
// Setup a progress report for thr ProgressBar
var _progressBarUpdate = new Progress<int>(value =>
{
this.SearchProgressBar.Value = value;
this.SearchProgressBar.Refresh();
});
var _progressFileNameUpdate = new Progress<string>(value =>
{
this.lblSearchFile.Text = "Searching File For : " + value;
this.lblSearchFile.Refresh();
});
// Start search on a backgroud thread and report progress as it occurs
Task.Run(async () => await this.SearchByStringAsync(p_searchParameter, _progressBarUpdate, _progressFileNameUpdate));
}
private async Task SearchByStringAsync(string p_searchParameter, IProgress<int> p_progressBar, IProgress<string> p_progressFileName)
{
await Task.Delay(1);
TextFileReader textFileReader = new TextFileReader();
LogSearchEventArguments logSearchEventArguments = null;
long _sessionloopCount = 0;
long _totalTextLinesCount = this.GetTotalSearchCount(p_searchParameter, SearchType.TextString);
// Get file names from SQL table
var _logFiles = DataOperations.LogFileSortableList(null);
foreach (var log in _logFiles)
{
// Format a file name to be read from the file system
string _fileName = log.Directory + "\\" + log.FileName;
p_progressFileName.Report(log.FileName);
// If we've raised an event for this file, then stop iterating over remaning text
if (logSearchEventArguments != null)
{
logSearchEventArguments = null;
break;
}
// Read in file contents from file system
List<string> _fileContents = textFileReader.ReadAndReturnStringList(_fileName);
long _fileTotalRecordCount = _fileContents.Count;
long _fileRecordCount = 0;
foreach (var _line in _fileContents)
{
if (_line.ToUpper().Contains(p_searchParameter.ToUpper()))
{
// Raise an event so search parameter and file name can be captured in another form
logSearchEventArguments =
new LogSearchEventArguments
(
"TextSearch", p_searchParameter, SearchType.TextString, true, log,
new DateTime(
Convert.ToInt32("20" + log.FileName.Substring(14, 2)),
Convert.ToInt32(log.FileName.Substring(16, 2)),
Convert.ToInt32(log.FileName.Substring(18, 2)))
);
// We found a match, so no further searching is needed in this log file,
// and it's been flagged in the DB, so raise the event to save search parameter and file name
// then break out of this loop to get the next file to search in.
this.OnSearchSucceeded(logSearchEventArguments);
break;
}
// These calcs are based on actual searches performed
_fileRecordCount++;
_sessionloopCount++;
p_progressBar.Report(Convert.ToInt32((_sessionloopCount * 100) / _totalTextLinesCount));
}
// Because we exit a search as soon as the 1st match is made, need to resynch all counts
// and update the progress bar accordingly
if (_fileRecordCount < _fileTotalRecordCount)
{
long _countDifference = _fileTotalRecordCount - _fileRecordCount;
// Add count difference to sessionLoopCount and update progress bar
_sessionloopCount += _countDifference;
p_progressBar.Report(Convert.ToInt32((_sessionloopCount * 100) / _totalTextLinesCount));
}
}
//Search is complete set Progress to 100% and report before exiting
p_progressBar.Report(100);
// Close the modal SearchForm and exit
this.Close();
}
}
I solved this problem but I'm still not certain of what caused it. I eliminated the method "private void SearchByText(string p_searchParameter)" and moved the code there into the btnSearch_Click event handler so I could call my background worker "SearchByStringAsync" directly from the button click event handler.
I also updated the EFCore NuGet Packages, which were version Net6.0 to version 6.0.4, because of single line of code in my Async background method, "var _logFiles = DataOperations.LogFileSortableList(null)".
That call returned a Sortable BindingList, using BindingList <T>. Between the NuGet updates and a minor change on a custom comparer method in my BindingList <T> class, the windows modal form now updates the ProgressBar and Label text as expected, and the form now responds immediately to user interaction.
With the CHtmlView class I was able to select all the text and copy to the clipboard like this:
void CChristianLifeMinistryHtmlView::CopyToClipboard()
{
ExecWB(OLECMDID_COPY, OLECMDEXECOPT_DONTPROMPTUSER, nullptr, nullptr);
}
void CChristianLifeMinistryHtmlView::SelectAll()
{
ExecWB(OLECMDID_SELECTALL, OLECMDEXECOPT_DONTPROMPTUSER, nullptr, nullptr);
}
I am trying to find out how to do the same with the new WebView2 API.
Update
The WebView2 control supports by design:
CTRL + A to select all the content.
CTRL + C to copy the selected text to the clipboard.
I found a VB.NET solution to programatically copy all the page to the clipboard:
Sub Async GetText()
v = Await wv.ExecuteScriptAsync("document.body.innerText")
End Sub
But I am using Visual C++ and I do not see this method exposed. Also, I am not sure it is what I want because I do not want to copy as plain text data but HTML data (suitable for pasting in Word) like with the hotkeys. I have made a GitHub issue for this too.
Update
So I have now tried this code but it does not appear to do anything:
void CWebBrowser::CopyToClipboard()
{
if (m_pImpl->m_webView != nullptr)
{
m_pImpl->m_webView->ExecuteScript(_T("document.body.innerText"), nullptr);
}
}
Update
According to this article it states:
Alternately, for ICoreWebView2::ExecuteScript, you provide an instance that has an Invoke method that provides you with the success or failure code of the ExecuteScript request. Also provide the second parameter that is the JSON of the result of running the script.
Trying to find example.
Update
I now understand that I need to do something like this:
void CWebBrowser::CopyToClipboard()
{
if (m_pImpl->m_webView != nullptr)
{
m_pImpl->m_webView->ExecuteScript(L"document.body.innerText",
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
[](HRESULT error, PCWSTR result) -> HRESULT
{
if (error != S_OK) {
ShowFailure(error, L"ExecuteScript failed");
}
SetClipboardText(result);
AfxMessageBox(L"HTML copied to clipboard!", MB_OK);
return S_OK;
}).Get());
}
}
The variable result has the contents of the HTML page. But it does not like my call to SetClipboardText now. This is the error:
This is the function:
void CWebBrowser::SetClipboardText(CString strText)
{
BYTE* pbyText;
TCHAR* pcBuffer;
HANDLE hText;
UINT uLength;
//USES_CONVERSION ;
if (::OpenClipboard(nullptr))
{
// Empty it of all data first.
::EmptyClipboard();
// Replace previous text contents.
uLength = strText.GetLength();
//pcBuffer = T2A( (LPTSTR)(LPCTSTR)strText);
pcBuffer = strText.GetBuffer(uLength);
if (pcBuffer != nullptr)
{
hText = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, (uLength + 1) * sizeof(TCHAR));
if (hText != nullptr)
{
pbyText = (BYTE*)::GlobalLock(hText);
if (pbyText != nullptr)
{
// Deliberately not _tcscpy().
//strcpy_s( (char *)pbyText, uLength+1, pcBuffer);
_tcscpy_s((TCHAR*)pbyText, uLength + 1, pcBuffer);
::GlobalUnlock(hText);
::SetClipboardData(CF_UNICODETEXT, hText);
// Don't free this memory, clipboard owns it now.
}
}
}
::CloseClipboard();
strText.ReleaseBuffer(uLength);
}
}
This is the only way I know (to date) to copy to clipboard. So I have two issues here:
It will not let me use this function.
I expect it will copy just text (CF_UNICODETEXT) and I don't know if that is sufficient for pasting the data as HTML into Word?
Concerning issue 1, I needed to make the method static. Then it compiled and copied the information to the clipboard.
Concerning issue 2, as expected, the pasted data was stripped HTML and I was left with just the text. And the new line characters were displayed as "\n" in the text. It was one huge paragraph. So I do indeed need the CF_HTML format.
It is a pity that the WebView2 does not expose the copy to clipboard feature that it already has functional.
Update
This is the clipboard method that I found on the internet for CF_HTML:
// CopyHtml() - Copies given HTML to the clipboard.
// The HTML/BODY blanket is provided, so you only need to
// call it like CopyHtml("<b>This is a test</b>");
void CopyHTML(char* html)
{
// Create temporary buffer for HTML header...
char* buf = new char[400 + strlen(html)];
if (!buf) return;
// Get clipboard id for HTML format...
static int cfid = 0;
if (!cfid) cfid = RegisterClipboardFormat("HTML Format");
// Create a template string for the HTML header...
strcpy(buf,
"Version:0.9\r\n"
"StartHTML:00000000\r\n"
"EndHTML:00000000\r\n"
"StartFragment:00000000\r\n"
"EndFragment:00000000\r\n"
"<html><body>\r\n"
"<!--StartFragment -->\r\n");
// Append the HTML...
strcat(buf, html);
strcat(buf, "\r\n");
// Finish up the HTML format...
strcat(buf,
"<!--EndFragment-->\r\n"
"</body>\r\n"
"</html>");
// Now go back, calculate all the lengths, and write out the
// necessary header information. Note, wsprintf() truncates the
// string when you overwrite it so you follow up with code to replace
// the 0 appended at the end with a '\r'...
char* ptr = strstr(buf, "StartHTML");
wsprintf(ptr + 10, "%08u", strstr(buf, "<html>") - buf);
*(ptr + 10 + 8) = '\r';
ptr = strstr(buf, "EndHTML");
wsprintf(ptr + 8, "%08u", strlen(buf));
*(ptr + 8 + 8) = '\r';
ptr = strstr(buf, "StartFragment");
wsprintf(ptr + 14, "%08u", strstr(buf, "<!--StartFrag") - buf);
*(ptr + 14 + 8) = '\r';
ptr = strstr(buf, "EndFragment");
wsprintf(ptr + 12, "%08u", strstr(buf, "<!--EndFrag") - buf);
*(ptr + 12 + 8) = '\r';
// Now you have everything in place ready to put on the clipboard.
// Open the clipboard...
if (OpenClipboard(0))
{
// Empty what's in there...
EmptyClipboard();
// Allocate global memory for transfer...
HGLOBAL hText = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, strlen(buf) + 4);
// Put your string in the global memory...
char* ptr = (char*)GlobalLock(hText);
strcpy(ptr, buf);
GlobalUnlock(hText);
::SetClipboardData(cfid, hText);
CloseClipboard();
// Free memory...
GlobalFree(hText);
}
// Clean up...
delete[] buf;
}
But it is not compatible with the PCWSTR variable.
Update
I have now realised after doing debugging that ExecuteScript does return the actual HTML data if I use document.body.innerText. The resulting string is just text with \n characters for the new lines. It is not in HTML format. So I am back to square one.
Just use ICoreWebView2::ExecuteScript.
I am showing this as an alternative because the current approach does not include the actual HTML content. The information I found on the internet was misleading about using document.body.innerText.
One solution to this is to use the document.execCommand() route. I added the following methods to my browser class:
void CWebBrowser::SelectAll()
{
if (m_pImpl->m_webView != nullptr)
{
m_pImpl->m_webView->ExecuteScript(L"document.execCommand(\"SelectAll\")", nullptr);
}
}
void CWebBrowser::CopyToClipboard2()
{
if (m_pImpl->m_webView != nullptr)
{
m_pImpl->m_webView->ExecuteScript(L"document.execCommand(\"Copy\")", nullptr);
}
}
Then I added the following button handlers to my dialog to test:
void CMFCTestWebView2Dlg::OnBnClickedButtonSelectAll()
{
if (m_pWebBrowser != nullptr)
{
m_pWebBrowser->SelectAll();
}
}
void CMFCTestWebView2Dlg::OnBnClickedButtonCopyToClipboard()
{
if (m_pWebBrowser != nullptr)
{
m_pWebBrowser->CopyToClipboard2();
}
}
This does work and appears to be the simplest solution for now. I see references to the Calendar API as an alternative but I am not sure yet how to implement that approach.
My only concern with execCommand is that I have seen it documented as deprecated.
I am developing an Bootstrapper application. When we move to the installation location selection wizard, we can have a browse option to change the location of our setup installation.
When we clock that browse option, i need to show only the drives( 'C', 'D' and 'F') available in a system. When we select the drive, it will not expand. i need that drive alone (Ex: C:) in the installation location text in the installation wizard.
Can anyone please guide me that how to achieve my requirement?
My code:
private void btn_Browse_Click(object sender, EventArgs e)
{
txt_InstallLocation.Focus();
FBD_Source.RootFolder = Environment.SpecialFolder.Desktop;
if (FBD_Source.ShowDialog() == DialogResult.OK)
{
txt_InstallLocation.TextBox.Text = FBD_Source.SelectedPath;
BA.Model.Bootstrapper.Engine.StringVariables["APPDIR"] = FBD_Source.SelectedPath + "\\";
}
}
I need to modify the below line of code.
FBD_Source.RootFolder = Environment.SpecialFolder.Desktop;
Can anyone please assist me to proceed further or direct me in right path?
Your can find the available drives like this.
var drives = System.Environment.GetLogicalDrives();
foreach (string d in drives)
Console.WriteLine(d);
Don't use the FolderBrowserDialog.
Create a form with a dynamically created radiobutton/list and make the user select one of those options
Something like this
var drives = System.Environment.GetLogicalDrives();
Form selectionForm = new Form() { MaximizeBox = false, FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog, StartPosition = FormStartPosition.CenterScreen };
ListBox lb = new ListBox() { Dock = DockStyle.Fill };
foreach (var item in drives)
lb.Items.Add(item);
selectionForm.Controls.Add(lb);
Button btnOk = new Button() { Dock = DockStyle.Left, Width = selectionForm.Width / 2 };
btnOk.Text = "OK";
Button btnCancel = new Button() { Dock = DockStyle.Right, Width = selectionForm.Width / 2 };
btnCancel.Text = "Cancel";
Panel bottomPanel = new Panel() { Dock = DockStyle.Bottom, Height = 50 };
bottomPanel.Controls.Add(btnOk);
bottomPanel.Controls.Add(btnCancel);
selectionForm.Controls.Add(bottomPanel);
selectionForm.ShowDialog();
MessageBox.Show(lb.SelectedItem.ToString());
how would a monitor just a particular file in AppData folder.
I've tried using StorageFolderQueryResult.ContentsChanged event to handle this, but this one actually callbacks for any changes through the folder.
My problem is to just a monitor a single file and use the eventhandler when its changed.
I've tried to use this "UserSearchFilter" property to QueryOptions. That didnt work actually.
cAn someone help with this ? It would also be helpful if you could additionally provide the syntax for the whole problem.
My contentschanged event is not firing on modifying the "filename" in the folder.
auto fileTypeFilter = ref new Platform::Collections::Vector<Platform::String^>();
fileTypeFilter->Append("*");
auto queryOptions = ref new QueryOptions(CommonFileQuery::OrderBySearchRank, fileTypeFilter);
//use the user's input to make a query
queryOptions->UserSearchFilter = InputTextBox->Text;
auto queryResult = musicFolder->CreateFileQueryWithOptions(queryOptions);
auto localFolder = ApplicationData::Current->LocalFolder;
auto currPath = localFolder->Path;
auto fileTypeFilter = ref new Platform::Collections::Vector<Platform::String^>();
fileTypeFilter->Append(".dat");
auto queryOptions = ref new QueryOptions(CommonFileQuery::OrderByName, fileTypeFilter);
//use the user's input to make a query
queryOptions->UserSearchFilter = L"filename";
auto queryResult = localFolder->CreateFileQueryWithOptions(queryOptions);
queryResult->ContentsChanged += ref new TypedEventHandler<IStorageQueryResultBase^, Platform::Object^>(this, &Scenario1::OnLocalAppDataChanged);
//find all files that match the query
create_task(queryResult->GetFilesAsync()).then([this, queryOptions] (IVectorView<StorageFile^>^ files)
{
String^ outputText = "";
//output how many files that match the query were found
if ( files->Size == 0)
{
outputText += "No files found for '" + queryOptions->UserSearchFilter + "'";
}
else if (files->Size == 1)
{
outputText += files->Size.ToString() + " file found:\n\n";
}
else
{
outputText += files->Size.ToString() + " files found:\n\n";
}
//output the name of each file that matches the query
for (unsigned int i = 0; i < files->Size; i++)
{
outputText += files->GetAt(i)->Name + "\n";
}
OutputTextBlock->Text = outputText;
});
}
void Scenario1::OnLocalAppDataChanged(Windows::Storage::Search::IStorageQueryResultBase^ sender, Platform::Object^ args)
{
Platform::String^ hello = L"hello world, I'm called back";
}
You have to call the method GetFilesAsync() at least once, otherwise the event will never fire.
Add
queryResult->GetFilesAsync();
before
queryResult->ContentsChanged += ref new TypedEventHandler<IStorageQueryResultBase^,...
I know you don't really need the files at that point, but that's the offical way the ContentsChanged event should be used. Have a look at the first paragraph of the documentation.
I have created a view that shows lost connection messages to users which pops over the current view. I want to update the view periodically based on connection status changes.
I can properly get the view and change the text of a label (verified with WriteLines), but nothing changes on the actual display. I even tried removing the view and readding it and calling SetNeedsDisplay, but nothing seems to help.
I have a global variable called OverView:
public static UIView OverView;
I create the label subview, add it to the overview and pop the overview in front of the current view:
UILabel labelTitle = new UILabel();
labelTitle.Text = title;
UIView labelTitleView = (UIView) labelTitle;
labelTitleView.Tag = 5000;
OverView.AddSubview(labelTitleView);
curView.InsertSubviewAbove(OverView, curView);
curView.BringSubviewToFront(OverView);
And then at a later time, I try to modify it like this from another function:
if ((OverView != null) && (OverView.Subviews != null))
{
for (int i = 0; i < OverView.Subviews.Length; i++)
{
WriteToConsole("Type: " + OverView.Subviews[i].GetType());
if (OverView.Subviews[i] is UILabel)
{
WriteToConsole("Found Label with Tag: " + ((UILabel)(OverView.Subviews[i])).Tag + " Text: " + ((UILabel)(OverView.Subviews[i])).Text);
if (((UILabel)(OverView.Subviews[i])).Tag == 5000)
{
WriteToConsole("Setting subview Title to: " + lostConnectionTitle);
lock (overViewLocker)
{
appReference.InvokeOnMainThread(delegate
{
UILabel tempLabel = ((UILabel)(OverView.Subviews[i]));
tempLabel.Text = lostConnectionTitle;
OverView.Subviews[i].RemoveFromSuperview();
OverView.AddSubview(tempLabel);
OverView.BringSubviewToFront(tempLabel);
OverView.SetNeedsLayout();
OverView.SetNeedsDisplay();
WriteToConsole("SetNeedsDisplay");
});
}
}
}
}
}
Have you tried to use delegate methods on your label, and change their value when events occur ?
For example, if your event is clicking on a button, you should have something like that:
yourLabel.Text = "Init";
buttonExample.TouchUpInside += (sender, e) => {
yourLabel.Text = "I touched my button";
};
When your View loads, you'll see "Init" and your button and once you click on it, the label text changed.
Xamarin has some explanation about events and delegate methods here.
I hope that helped.