This is a request for support for WP8 and Unity 5 for the Facebook SDK.
I am a developer wanting to produce games across many platforms, and being able to publish to the Windows Phone 8 store is a big part of my agenda.
Is support for this platform scheduled to be released any time soon? If so, when are you looking to release it?
Also, is there an imminent release for the Unity SDK that is designed to work with Unity 5? I have managed to get the current release to work, but as it is not yet fully supported, I do not know what will work and what won't.
Any information regarding these issues would be much appreciated!
Update: I ran into this video that explains how the "AndContinue" methods are going away in windows 10 so we can use one single method; the Async methods that already exists on windows 8/8.1 api. Check it out at https://youtu.be/aFVAP3fNJVo?t=23m34s
I was in the same place as you but managed to get it working. When I have time, ill probably write a blog on my experiences.
Here is some of the main code that directly calls on WebAuthenticationBroker:-
static bool isTryingToRegister { get; set; }
private static string _FbToken;
public static string response;
static bool isLoggedIn { get; set; }
static private string AppID { get { return "000000000000000000"; } }
static private Uri callback { get; set; }
static private string permissions { get { return "public_profile, user_friends, email, publish_actions, user_photos"; } }
public static System.Action<object> AuthSuccessCallback;
public static System.Action<object> AuthFailedCallback;
public static string fbToken
{
get
{
return _FbToken;
}
}
static Uri loginUrl
{
get
{
return new Uri(String.Format("https://www.facebook.com/v2.0/dialog/oauth/?client_id={0}&display=popup&response_type=token&redirect_uri={1}&scope={2}",
AppID,
callback,
permissions));
}
}
public static IEnumerator _Run_ConnectWithFacebook()
{
yield return new WaitForEndOfFrame();
#if UNITY_EDITOR
UnityDebugAuthentication();
#endif
#if NETFX_CORE
WindowsStoreAuthenticate();
#endif
}
#if NETFX_CORE
static void WindowsStoreAuthenticate()
{
#if UNITY_WP_8_1 && !UNITY_EDITOR
AuthSuccessCallback = _Run_ConnectWithFacebook_SuccessResponse;
AuthFailedCallback = _Run_ConnectWithFacebook_FailedResponse;
UnityEngine.WSA.Application.InvokeOnUIThread(
() =>
{
callback = WebAuthenticationBroker.GetCurrentApplicationCallbackUri();
WebAuthenticationBroker.AuthenticateAndContinue(loginUrl, callback, null, WebAuthenticationOptions.None);
}, true);
#endif
#if UNITY_METRO_8_1 && !UNITY_EDITOR
AuthSuccessCallback = _Run_ConnectWithFacebook_SuccessResponse;
AuthFailedCallback = _Run_ConnectWithFacebook_FailedResponse;
UnityEngine.WSA.Application.InvokeOnUIThread(
async () =>
{
callback = WebAuthenticationBroker.GetCurrentApplicationCallbackUri();
WebAuthenticationResult authResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, loginUrl);
}, true);
#endif
}
#endif
Note how I use WebAuthenticationBroker.AuthenticateAndContinue for windows phone and WebAuthenticationBroker.AuthenticateAsync for windows 8. This is necessary for their respective platforms. You may want to look into getting the WebAuthenticationBroker which is a Microsoft class that works on WP8 and WSA (im guessing you are aiming for WSA, so am I):-
https://msdn.microsoft.com/library/windows/apps/windows.security.authentication.web.webauthenticationbroker.aspx?f=255&MSPPError=-2147217396
You will also need to build a c# project in unity so you can implement part 2 of it which includes handling when the token details are returned to the app. Here is my code sample of App.xaml.cs which was inspired by https://msdn.microsoft.com/en-us/library/dn631755.aspx:-
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using UnityPlayer;
using Template.Common;
using Windows.Security.Authentication.Web;
// The Blank Application template is documented at http://go.microsoft.com/fwlink/?LinkId=234227
namespace Template
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
sealed partial class App : Application
{
private WinRTBridge.WinRTBridge _bridge;
private AppCallbacks appCallbacks;
#if UNITY_WP_8_1
public ContinuationManager continuationManager { get; private set; }
#endif
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
appCallbacks = new AppCallbacks();
appCallbacks.RenderingStarted += RemoveSplashScreen;
#if UNITY_WP_8_1
this.Suspending += OnSuspending;
continuationManager = new ContinuationManager();
#endif
}
/// <summary>
/// Invoked when application is launched through protocol.
/// Read more - http://msdn.microsoft.com/library/windows/apps/br224742
/// </summary>
/// <param name="args"></param>
protected override void OnActivated(IActivatedEventArgs args)
{
#if UNITY_WP_8_1
var continuationEventArgs = args as IContinuationActivatedEventArgs;
if (continuationEventArgs != null)
{
ContinueWebAuthentication(args as WebAuthenticationBrokerContinuationEventArgs);
return;
//}
}
#endif
string appArgs = "";
Windows.ApplicationModel.Activation.SplashScreen splashScreen = null;
switch (args.Kind)
{
case ActivationKind.Protocol:
ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs;
splashScreen = eventArgs.SplashScreen;
appArgs += string.Format("Uri={0}", eventArgs.Uri.AbsoluteUri);
break;
}
InitializeUnity(appArgs, splashScreen);
}
#if UNITY_WP_8_1
public void ContinueWebAuthentication(WebAuthenticationBrokerContinuationEventArgs args)
{
WebAuthenticationResult result = args.WebAuthenticationResult;
if (result.ResponseStatus == WebAuthenticationStatus.Success)
{
string responseData = result.ResponseData.Substring(result.ResponseData.IndexOf("access_token"));
String[] keyValPairs = responseData.Split('&');
string access_token = null;
string expires_in = null;
for (int i = 0; i < keyValPairs.Length; i++)
{
String[] splits = keyValPairs[i].Split('=');
switch (splits[0])
{
case "access_token":
access_token = splits[1]; //you may want to store access_token for further use. Look at Scenario5 (Account Management).
break;
case "expires_in":
expires_in = splits[1];
break;
}
}
AppCallbacks.Instance.UnityActivate(Window.Current.CoreWindow, CoreWindowActivationState.CodeActivated);
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//function to call or variable that accepts the access token
}, false);
//OutputToken(result.ResponseData.ToString());
//await GetFacebookUserNameAsync(result.ResponseData.ToString());
}
else if (result.ResponseStatus == WebAuthenticationStatus.ErrorHttp)
{
//OutputToken("HTTP Error returned by AuthenticateAsync() : " + result.ResponseErrorDetail.ToString());
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//function to call indicating something went wrong
}, false);
}
else if(result.ResponseStatus == WebAuthenticationStatus.UserCancel)
{
//OutputToken("Error returned by AuthenticateAsync() : " + result.ResponseStatus.ToString());
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//function to call indicating something went wrong
}, false);
}
}
#endif
/// <summary>
/// Invoked when application is launched via file
/// Read more - http://msdn.microsoft.com/library/windows/apps/br224742
/// </summary>
/// <param name="args"></param>
protected override void OnFileActivated(FileActivatedEventArgs args)
{
string appArgs = "";
Windows.ApplicationModel.Activation.SplashScreen splashScreen = null;
splashScreen = args.SplashScreen;
appArgs += "File=";
bool firstFileAdded = false;
foreach (var file in args.Files)
{
if (firstFileAdded) appArgs += ";";
appArgs += file.Path;
firstFileAdded = true;
}
InitializeUnity(appArgs, splashScreen);
}
/// <summary>
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used when the application is launched to open a specific file, to display
/// search results, and so forth.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
InitializeUnity(args.Arguments, args.SplashScreen);
}
private void InitializeUnity(string args, Windows.ApplicationModel.Activation.SplashScreen splashScreen)
{
#if UNITY_WP_8_1
ApplicationView.GetForCurrentView().SuppressSystemOverlays = true;
#pragma warning disable 4014
StatusBar.GetForCurrentView().HideAsync();
#pragma warning restore 4014
#endif
appCallbacks.SetAppArguments(args);
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null && !appCallbacks.IsInitialized())
{
var mainPage = new MainPage(splashScreen);
Window.Current.Content = mainPage;
Window.Current.Activate();
// Setup scripting bridge
_bridge = new WinRTBridge.WinRTBridge();
appCallbacks.SetBridge(_bridge);
#if !UNITY_WP_8_1
appCallbacks.SetKeyboardTriggerControl(mainPage);
#endif
appCallbacks.SetSwapChainPanel(mainPage.GetSwapChainPanel());
appCallbacks.SetCoreWindowEvents(Window.Current.CoreWindow);
appCallbacks.InitializeD3DXAML();
}
Window.Current.Activate();
#if UNITY_WP_8_1
SetupLocationService();
#endif
}
private void RemoveSplashScreen()
{
// This will fail if you change main window class
// Make sure to adjust accordingly if you do something like this
MainPage page = (MainPage)Window.Current.Content;
page.RemoveSplashScreen();
}
#if UNITY_WP_8_1
// This is the default setup to show location consent message box to the user
// You can customize it to your needs, but do not remove it completely if your application
// uses location services, as it is a requirement in Windows Store certification process
private async void SetupLocationService()
{
if (!appCallbacks.IsLocationCapabilitySet())
{
return;
}
const string settingName = "LocationContent";
bool userGaveConsent = false;
object consent;
var settings = Windows.Storage.ApplicationData.Current.LocalSettings;
var userWasAskedBefore = settings.Values.TryGetValue(settingName, out consent);
if (!userWasAskedBefore)
{
var messageDialog = new Windows.UI.Popups.MessageDialog("Can this application use your location?", "Location services");
var acceptCommand = new Windows.UI.Popups.UICommand("Yes");
var declineCommand = new Windows.UI.Popups.UICommand("No");
messageDialog.Commands.Add(acceptCommand);
messageDialog.Commands.Add(declineCommand);
userGaveConsent = (await messageDialog.ShowAsync()) == acceptCommand;
settings.Values.Add(settingName, userGaveConsent);
}
else
{
userGaveConsent = (bool)consent;
}
if (userGaveConsent)
{ // Must be called from UI thread
appCallbacks.SetupGeolocator();
}
}
#endif
/// <summary>
/// Invoked when Navigation to a certain page fails
/// </summary>
/// <param name="sender">The Frame which failed navigation</param>
/// <param name="e">Details about the navigation failure</param>
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
/// <summary>
/// Invoked when application execution is being suspended. Application state is saved
/// without knowing whether the application will be terminated or resumed with the contents
/// of memory still intact.
/// </summary>
/// <param name="sender">The source of the suspend request.</param>
/// <param name="e">Details about the suspend request.</param>
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
await SuspensionManager.SaveAsync();
deferral.Complete();
}
}
}
You will also need the ContinuationManager.cs class and the SuspensionManager.cs class but mainly for WP8.1 Universal. This isnt needed for W8.1 and so you should ensure that you are using #defines to keep them within their own contexts
If you want, instead of calling on WebAuthenticationBroker, you could call on the facebook app directly but I don't know of all the details. You can read up on that on facebook's website developers.facebook.com/docs/facebook-login/login-for-windows-phone. That method isnt recommended incase the user doesn't have it installed.
Long (rushed) story short, call WebAuthenticationBroker, handle the continuation Event in OnActivated event to catch the WebAuthenticationBrokerContinuationEventArgs object coming through and use the data however you see fit. Be sure to use the following if you want to call any code in the unity side. Note that you can directly access the c# code from App.xaml.cs:-
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//your code here
}, false);
Also note that this is mainly to get the access token. Once we have that, we can make basic WWW calls directly to facebook and get data back from it. The data will be returned as a JSON format (ps this is such an awesome clean format!) you can use the .NET json library to serialize it into a class. I use json2csharp.com to convert any example output into a class which I just parse into the json library.
Related
I'm working through this tutorial: https://mtaulty.com/2019/07/18/simple-shared-holograms-with-photon-networking-part-1/ with the hope of reproducing the shared coordinate system between two Hololens 2 headsets. I'm using Unity 2020, PUN2, ARFoundation and MRTK.
Because the tutorial is using WorldAnchors (WSA platform), which is a bit old, I'm trying to modify it to use ARFoundation. So far, the code I have as a result, seems to properly have the two headsets communicating via PUN2, but the blue cube as shown in the tutorial does not align between the headsets. The cube simply seems referenced to each headsets initial startup frame of reference. Below is the code. I've kept everything as one-to-one with the tutorial as possible, except where I felt I needed to swap WorldAnchors for ARAnchors and also where I swapped in a SpatialAnchorManager class to handle the Azure Spatial Service session since I found the tutorial's StartSession function didn't seem to work properly. Both AzureSpatialAnchorService.cs and PhotonScript.cs are attached to a root game object in the scene. Picture of the scene attached. Based on the debug logs I'm able to tell that the first headset is creating and saving an anchor to Azure and the second headset is able to find that same anchor. But I apparently am not performing a necessary transformation between headsets?
Can anyone suggest what I'm doing wrong and/or what specific edits that need to be made to get spatial alignment between headsets?
Thanks!
AzureSpatialAnchorService.cs:
using Microsoft.Azure.SpatialAnchors.Unity;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.WSA;
namespace AzureSpatialAnchors
{
[RequireComponent(typeof(SpatialAnchorManager))]
public class AzureSpatialAnchorService : MonoBehaviour
{
[Serializable]
public class AzureSpatialAnchorServiceProfile
{
[SerializeField]
[Tooltip("The account id from the Azure portal for the Azure Spatial Anchors service")]
string azureAccountId;
public string AzureAccountId => this.azureAccountId;
[SerializeField]
[Tooltip("The access key from the Azure portal for the Azure Spatial Anchors service (for Key authentication)")]
string azureServiceKey;
public string AzureServiceKey => this.azureServiceKey;
}
[SerializeField]
[Tooltip("The configuration for the Azure Spatial Anchors Service")]
AzureSpatialAnchorServiceProfile profile = new AzureSpatialAnchorServiceProfile();
public AzureSpatialAnchorServiceProfile Profile => this.profile;
TaskCompletionSource<CloudSpatialAnchor> taskWaitForAnchorLocation;
//CloudSpatialAnchorSession cloudSpatialAnchorSession;
private SpatialAnchorManager _spatialAnchorManager = null;
public AzureSpatialAnchorService()
{
}
public async Task<string> CreateAnchorOnObjectAsync(GameObject gameObjectForAnchor)
{
string anchorId = string.Empty;
try
{
await this.StartSession();
Debug.Log("Started Session");
//Add and configure ASA components
CloudNativeAnchor cloudNativeAnchor = gameObjectForAnchor.AddComponent<CloudNativeAnchor>();
await cloudNativeAnchor.NativeToCloud();
Debug.Log("After NativeToCloud");
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);
// As per previous comment.
//Collect Environment Data
while (!_spatialAnchorManager.IsReadyForCreate)
{
float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
}
Debug.Log($"ASA - Saving room cloud anchor... ");
await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);
anchorId = cloudSpatialAnchor?.Identifier;
bool saveSucceeded = cloudSpatialAnchor != null;
if (!saveSucceeded)
{
Debug.LogError("ASA - Failed to save, but no exception was thrown.");
return anchorId;
}
anchorId = cloudSpatialAnchor.Identifier;
Debug.Log($"ASA - Saved room cloud anchor with ID: {anchorId}");
}
catch (Exception exception) // TODO: reasonable exceptions here.
{
Debug.Log("ASA - Failed to save room anchor: " + exception.ToString());
Debug.LogException(exception);
}
return (anchorId);
}
public async Task<bool> PopulateAnchorOnObjectAsync(string anchorId, GameObject gameObjectForAnchor)
{
bool anchorLocated = false;
try
{
await this.StartSession();
this.taskWaitForAnchorLocation = new TaskCompletionSource<CloudSpatialAnchor>();
var watcher = _spatialAnchorManager.Session.CreateWatcher(
new AnchorLocateCriteria()
{
Identifiers = new string[] { anchorId },
BypassCache = true,
Strategy = LocateStrategy.AnyStrategy,
RequestedCategories = AnchorDataCategory.Spatial
}
);
var cloudAnchor = await this.taskWaitForAnchorLocation.Task;
anchorLocated = cloudAnchor != null;
if (anchorLocated)
{
Debug.Log("Anchor located");
gameObjectForAnchor.AddComponent<CloudNativeAnchor>().CloudToNative(cloudAnchor);
Debug.Log("Attached Local Anchor");
}
watcher.Stop();
}
catch (Exception ex) // TODO: reasonable exceptions here.
{
Debug.Log($"Caught {ex.Message}");
}
return (anchorLocated);
}
/// <summary>
/// Start the Azure Spatial Anchor Service session
/// This must be called before calling create, populate or delete methods.
/// </summary>
public async Task<bool> StartSession()
{
//if (this.cloudSpatialAnchorSession == null)
//{
// Debug.Assert(this.cloudSpatialAnchorSession == null);
// this.ThrowOnBadAuthConfiguration();
// // setup the session
// this.cloudSpatialAnchorSession = new CloudSpatialAnchorSession();
// // set the Azure configuration parameters
// this.cloudSpatialAnchorSession.Configuration.AccountId = this.Profile.AzureAccountId;
// this.cloudSpatialAnchorSession.Configuration.AccountKey = this.Profile.AzureServiceKey;
// // register event handlers
// this.cloudSpatialAnchorSession.Error += this.OnCloudSessionError;
// this.cloudSpatialAnchorSession.AnchorLocated += OnAnchorLocated;
// this.cloudSpatialAnchorSession.LocateAnchorsCompleted += OnLocateAnchorsCompleted;
// // start the session
// this.cloudSpatialAnchorSession.Start();
//}
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
_spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
_spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
_spatialAnchorManager.AnchorLocated += OnAnchorLocated;
//_spatialAnchorManager.LocateAnchorsCompleted += OnLocateAnchorsCompleted;
await _spatialAnchorManager.StartSessionAsync();
return true;
}
/// <summary>
/// Stop the Azure Spatial Anchor Service session
/// </summary>
//public void StopSession()
//{
// if (this.cloudSpatialAnchorSession != null)
// {
// // stop session
// this.cloudSpatialAnchorSession.Stop();
// // clear event handlers
// this.cloudSpatialAnchorSession.Error -= this.OnCloudSessionError;
// this.cloudSpatialAnchorSession.AnchorLocated -= OnAnchorLocated;
// this.cloudSpatialAnchorSession.LocateAnchorsCompleted -= OnLocateAnchorsCompleted;
// // cleanup
// this.cloudSpatialAnchorSession.Dispose();
// this.cloudSpatialAnchorSession = null;
// }
//}
void OnLocateAnchorsCompleted(object sender, LocateAnchorsCompletedEventArgs args)
{
Debug.Log("On Locate Anchors Completed");
Debug.Assert(this.taskWaitForAnchorLocation != null);
if (!this.taskWaitForAnchorLocation.Task.IsCompleted)
{
this.taskWaitForAnchorLocation.TrySetResult(null);
}
}
void OnAnchorLocated(object sender, AnchorLocatedEventArgs args)
{
Debug.Log($"On Anchor Located, status is {args.Status} anchor is {args.Anchor?.Identifier}, pointer is {args.Anchor?.LocalAnchor}");
Debug.Assert(this.taskWaitForAnchorLocation != null);
this.taskWaitForAnchorLocation.SetResult(args.Anchor);
}
void OnCloudSessionError(object sender, SessionErrorEventArgs args)
{
Debug.Log($"On Cloud Session Error: {args.ErrorMessage}");
}
void ThrowOnBadAuthConfiguration()
{
if (string.IsNullOrEmpty(this.Profile.AzureAccountId) ||
string.IsNullOrEmpty(this.Profile.AzureServiceKey))
{
throw new ArgumentNullException("Missing required configuration to connect to service");
}
}
}
}
PhotonScript.cs:
using System;
using System.Threading.Tasks;
using AzureSpatialAnchors;
using ExitGames.Client.Photon;
using Photon.Pun;
using Photon.Realtime;
public class PhotonScript : MonoBehaviourPunCallbacks
{
enum RoomStatus
{
None,
CreatedRoom,
JoinedRoom,
JoinedRoomDownloadedAnchor
}
public int emptyRoomTimeToLiveSeconds = 120;
RoomStatus roomStatus = RoomStatus.None;
void Start()
{
PhotonNetwork.ConnectUsingSettings();
}
public override void OnConnectedToMaster()
{
base.OnConnectedToMaster();
var roomOptions = new RoomOptions();
roomOptions.EmptyRoomTtl = this.emptyRoomTimeToLiveSeconds * 1000;
PhotonNetwork.JoinOrCreateRoom(ROOM_NAME, roomOptions, null);
}
public async override void OnJoinedRoom()
{
base.OnJoinedRoom();
// Note that the creator of the room also joins the room...
if (this.roomStatus == RoomStatus.None)
{
this.roomStatus = RoomStatus.JoinedRoom;
}
await this.PopulateAnchorAsync();
}
public async override void OnCreatedRoom()
{
base.OnCreatedRoom();
this.roomStatus = RoomStatus.CreatedRoom;
await this.CreateAnchorAsync();
}
async Task CreateAnchorAsync()
{
// If we created the room then we will attempt to create an anchor for the parent
// of the cubes that we are creating.
var anchorService = this.GetComponent<AzureSpatialAnchorService>();
var anchorId = await anchorService.CreateAnchorOnObjectAsync(this.gameObject);
// Put this ID into a custom property so that other devices joining the
// room can get hold of it.
#if UNITY_2020
PhotonNetwork.CurrentRoom.SetCustomProperties(
new Hashtable()
{
{ ANCHOR_ID_CUSTOM_PROPERTY, anchorId }
}
);
#endif
}
async Task PopulateAnchorAsync()
{
if (this.roomStatus == RoomStatus.JoinedRoom)
{
object keyValue = null;
#if UNITY_2020
// First time around, this property may not be here so we see if is there.
if (PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue(
ANCHOR_ID_CUSTOM_PROPERTY, out keyValue))
{
// If the anchorId property is present then we will try and get the
// anchor but only once so change the status.
this.roomStatus = RoomStatus.JoinedRoomDownloadedAnchor;
// If we didn't create the room then we want to try and get the anchor
// from the cloud and apply it.
var anchorService = this.GetComponent<AzureSpatialAnchorService>();
await anchorService.PopulateAnchorOnObjectAsync(
(string)keyValue, this.gameObject);
}
#endif
}
}
public async override void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
{
base.OnRoomPropertiesUpdate(propertiesThatChanged);
await this.PopulateAnchorAsync();
}
static readonly string ANCHOR_ID_CUSTOM_PROPERTY = "anchorId";
static readonly string ROOM_NAME = "HardCodedRoomName";
}
From reviewing the code and scenario, I am reading that this is in same local area. So, the ASA service will have the shared anchor as detailed in docs:
https://learn.microsoft.com/en-us/windows/mixed-reality/design/shared-experiences-in-mixed-reality
"Shared static holograms (no interactions)
Leverage Azure Spatial Anchors in your app. Enabling and sharing spatial anchors across devices allows you to create an application where users see holograms in the same place at the same time. Additional syncing across devices is needed to enable users to interact with holograms and see movements or state updates of holograms."
The tutorial that Microsoft docs point to is here if it helps comparison to the other sample:
https://learn.microsoft.com/en-us/windows/mixed-reality/develop/unity/tutorials/mr-learning-sharing-01
This question already has answers here:
How to pass data (and references) between scenes in Unity
(6 answers)
Closed 2 years ago.
I have a name input script in the first scene, the plan is I want to call this input in the second scene, when I enter the name in the first scene, then the name will appear in the second scene too, how do you do that?
public class NamaUser : MonoBehaviour {
public InputField nama;
public Text teks;
public void NamaTeks () {
if (nama.text == "") {
teks.text = "Harap Isi Nama";
} else {
teks.text = "Namaku " + nama.text;
}
}
}
You can save the input's value PlayerPrefs.
Set the PlayerPrefs:
//Name of Pref in first parameter
//Value in second parameter
PlayerPrefs.SetString("value", teks.value);
Get the PlayerPref in second scene:
//Name of Pref in first parameter
//Returns value of PlayerPrefs
String a = PlayerPrefs.SetString("value");
Cons:
You can pass data not only between scenes but also between instances (game sessions).
Easy to manage since Unity handles all background process.
Can be used to store data to track highscores.
Pros:
Uses file system.
Data can easily be changed from prefs file.
Or, another way -- use Singelton and DontDestroyOnLoad()
Allows easy access to fields and saves an object between scenes.
For example use this template, to create your class.
using UnityEngine;
public class Singelton<T> : MonoBehaviour where T : Singelton<T>
{
private static T instance = null;
private bool alive = true;
public static T Instance
{
get
{
if (instance != null)
{
return instance;
}
else
{
//Find T
T[] managers = GameObject.FindObjectsOfType<T>();
if (managers != null)
{
if (managers.Length == 1)
{
instance = managers[0];
DontDestroyOnLoad(instance);
return instance;
}
else
{
if (managers.Length > 1)
{
Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
"But this is Singelton! Check project.");
for (int i = 0; i < managers.Length; ++i)
{
T manager = managers[i];
Destroy(manager.gameObject);
}
}
}
}
//create
GameObject go = new GameObject(typeof(T).Name, typeof(T));
instance = go.GetComponent<T>();
DontDestroyOnLoad(instance.gameObject);
return instance;
}
}
//Can be initialized externally
set
{
instance = value as T;
}
}
/// <summary>
/// Check flag if need work from OnDestroy or OnApplicationExit
/// </summary>
public static bool IsAlive
{
get
{
if (instance == null)
return false;
return instance.alive;
}
}
protected virtual void Awake()
{
if (instance == null)
{
DontDestroyOnLoad(gameObject);
instance = this as T;
}
else
{
Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
"But this is Singelton! Check project.");
Destroy(gameObject);
}
}
protected virtual void OnDestroy() { alive = false; }
protected virtual void OnApplicationQuit() { alive = false; }
}
Example of using:
class MyClass Settings : Singelton<Settings>
{
string param;
}
I am creating a Windows Phone 8.1 app using an MVVM design pattern and the MVVM Light toolkit.
I am trying to create a simple login page that takes a username and password from a user and authenticates with a webservice. During this authentication process I want to show a ProgressBar (loading dots) at the top of the page to indicate that something is happening.
I have successfully created my ViewModel and bound my View to allow me to control the visibility of the ProgressBar from a Command attached to my login button. This works, however, the UI only updates to show the ProgressBar once the entire Login process is completed, making the use of a progress indicator pointless.
How do I first set the visibility of the ProgressBar (and have the UI update) then go off an perform my Login process?
Here is some code:
xaml
<ProgressBar IsIndeterminate="True" Height="1" Visibility="{Binding Login.ProgressVisibility, UpdateSourceTrigger=PropertyChanged, FallbackValue=Collapsed}" />
ViewModel
public class LoginViewModel : ViewModelBase
{
private IDialogService dialogService;
private INavigationService navigationService;
private string _username;
private string _password;
private Visibility _progressVisibility = Visibility.Collapsed;
/// <summary>
/// User login name / email address
/// </summary>
public string Username
{
get
{
return _username;
}
set
{
Set(() => Username, ref _username, value);
}
}
/// <summary>
/// User password
/// </summary>
public string Password
{
get
{
return _password;
}
set
{
Set(() => Password, ref _password, value);
}
}
/// <summary>
/// Is loading in progress
/// </summary>
public Visibility ProgressVisibility
{
get
{
return _progressVisibility;
}
set
{
Set(() => ProgressVisibility, ref _progressVisibility, value);
}
}
/// <summary>
/// Perform login action
/// </summary>
public RelayCommand LoginCommand { get; private set; }
/// <summary>
/// constructor
/// </summary>
/// <param name="dialogService"></param>
public LoginViewModel(IDialogService dialogService, INavigationService navigationService)
{
this.LoginCommand = new RelayCommand(this.Login, CanLogin);
this.dialogService = dialogService;
this.navigationService = navigationService;
}
public void Login()
{
this.ProgressVisibility = Visibility.Visible;
//check the credentials have been provided
if (this._username == null || this._password == null)
{
//show dialogue
dialogService.ShowMessage("Please enter you login credentials", "Login");
}
else
{
//perform an authentication request with the service
ValidateLoginRequest request = new ValidateLoginRequest();
request.Device = new Device();
request.UserName = this.Username;
request.Password = this.Password;
var response = Task.Run(() => LoginAsync(request)).GetAwaiter().GetResult();
if (response != null)
{
if (response.IsSuccessful)
{
navigationService.NavigateTo("Main");
}
else
{
dialogService.ShowMessage(response.ErrorCode + " :" + response.ErrorMessage, "Login");
}
}
else
{
//failure
dialogService.ShowMessage("ECOM000: An unknown error has occurred", "Login");
}
}
}
async Task<ValidateLoginResponse> LoginAsync(ValidateLoginRequest request)
{
Model.RuntimeDataService proxy = new Model.RuntimeDataService();
ValidateLoginResponse response = (ValidateLoginResponse)await proxy.PostAsync(request, typeof(ValidateLoginResponse), "ValidateLogin");
return response;
}
public bool CanLogin()
{
return true;
}
}
I solved this using Kotlin by adding my own CustomBindingAdapter:
#BindingAdapter("android:visibility")
fun setVisibility(view: View, visible: Boolean) {
view.visibility = if (visible) View.INVISIBLE else View.VISIBLE
}
Inside your view.xml:
First add data variable:
<data>
<variable name="loginViewModel" type="yourpackage.viewmodel.LoginViewModel"/>
</data>
<ProgressBar
android:id="#+id/login_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:visibility="#{!loginViewModel.loginProgress}" />
Your Activity must have:
val binding = DataBindingUtil.setContentView<LoginViewBinding>(this, R.layout.login_view)
val loginViewModel = LoginViewModel(this)
binding.setLoginViewModel(loginViewModel)
At the end your ViewModel needs to handle the visibility:
var loginProgress = ObservableField<Boolean>()
fun doSomething(){
this.loginProgress.set(true)
}
And set loginProgress.set(false) wherever you need to stop the progressBar.
I hope this can help someone else ;)
You are creating a background Task and then causing the UI thread to wait for the response.
Try awaiting the Task returned from the LoginAsync method:
var response = await LoginAsync(request);
This should allow your UI thread to continue, with the rest of the method acting as a callback.
You will probably need to add the async keyword to the Login method.
I love that Windows 10 now has support for virtual desktops built in, but I have some features that I'd like to add/modify (e.g., force a window to appear on all desktops, launch the task view with a hotkey, have per-monitor desktops, etc.)
I have searched for applications and developer references to help me customize my desktops, but I have had no luck.
Where should I start? I am looking for Windows API functions (ideally, that are callable from a C# application) that will give me programmatic access to manipulate virtual desktops and the windows therein.
The Windows SDK Support Team Blog posted a C# demo to switch Desktops via IVirtualDesktopManager:
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
[System.Security.SuppressUnmanagedCodeSecurity]
public interface IVirtualDesktopManager
{
[PreserveSig]
int IsWindowOnCurrentVirtualDesktop(
[In] IntPtr TopLevelWindow,
[Out] out int OnCurrentDesktop
);
[PreserveSig]
int GetWindowDesktopId(
[In] IntPtr TopLevelWindow,
[Out] out Guid CurrentDesktop
);
[PreserveSig]
int MoveWindowToDesktop(
[In] IntPtr TopLevelWindow,
[MarshalAs(UnmanagedType.LPStruct)]
[In]Guid CurrentDesktop
);
}
[ComImport, Guid("aa509086-5ca9-4c25-8f95-589d3c07b48a")]
public class CVirtualDesktopManager
{
}
public class VirtualDesktopManager
{
public VirtualDesktopManager()
{
cmanager = new CVirtualDesktopManager();
manager = (IVirtualDesktopManager)cmanager;
}
~VirtualDesktopManager()
{
manager = null;
cmanager = null;
}
private CVirtualDesktopManager cmanager = null;
private IVirtualDesktopManager manager;
public bool IsWindowOnCurrentVirtualDesktop(IntPtr TopLevelWindow)
{
int result;
int hr;
if ((hr = manager.IsWindowOnCurrentVirtualDesktop(TopLevelWindow, out result)) != 0)
{
Marshal.ThrowExceptionForHR(hr);
}
return result != 0;
}
public Guid GetWindowDesktopId(IntPtr TopLevelWindow)
{
Guid result;
int hr;
if ((hr = manager.GetWindowDesktopId(TopLevelWindow, out result)) != 0)
{
Marshal.ThrowExceptionForHR(hr);
}
return result;
}
public void MoveWindowToDesktop(IntPtr TopLevelWindow, Guid CurrentDesktop)
{
int hr;
if ((hr = manager.MoveWindowToDesktop(TopLevelWindow, CurrentDesktop)) != 0)
{
Marshal.ThrowExceptionForHR(hr);
}
}
}
it includes the API to detect on which desktop the Window is shown and it can switch and move a Windows the a Desktop.
Programmatic access to the virtual desktop feature is very limited, as Microsoft has only exposed the IVirtualDesktopManager COM interface. It does provide two key functions:
IVirtualDesktopManager::GetWindowDesktopId allows you to retrieve the ID of a virtual desktop, based on a window that is already assigned to that desktop.
IVirtualDesktopManager::MoveWindowToDesktop allows you to move a window to a specific virtual desktop.
Unfortunately, this is not nearly enough to accomplish anything useful. I've written some C# code based on the reverse-engineering work done by NickoTin. I can't read much of the Russian in his blog post, but his C++ code was pretty accurate.
I do need to emphasize that this code is not something you want to commit to in a product. Microsoft always feels free to change undocumented APIs whenever they feel like it. And there is a runtime risk as well: this code does not necessarily interact well when the user is tinkering with the virtual desktops. Always keep in mind that a virtual desktop can appear and disappear at any time, completely out of sync with your code.
To use the code, create a new C# class library project. I'll first post ComInterop.cs, it contains the COM interface declarations that match NickoTin's C++ declarations:
using System;
using System.Runtime.InteropServices;
namespace Windows10Interop {
internal static class Guids {
public static readonly Guid CLSID_ImmersiveShell =
new Guid(0xC2F03A33, 0x21F5, 0x47FA, 0xB4, 0xBB, 0x15, 0x63, 0x62, 0xA2, 0xF2, 0x39);
public static readonly Guid CLSID_VirtualDesktopManagerInternal =
new Guid(0xC5E0CDCA, 0x7B6E, 0x41B2, 0x9F, 0xC4, 0xD9, 0x39, 0x75, 0xCC, 0x46, 0x7B);
public static readonly Guid CLSID_VirtualDesktopManager =
new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A");
public static readonly Guid IID_IVirtualDesktopManagerInternal =
new Guid("AF8DA486-95BB-4460-B3B7-6E7A6B2962B5");
public static readonly Guid IID_IVirtualDesktop =
new Guid("FF72FFDD-BE7E-43FC-9C03-AD81681E88E4");
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("FF72FFDD-BE7E-43FC-9C03-AD81681E88E4")]
internal interface IVirtualDesktop {
void notimpl1(); // void IsViewVisible(IApplicationView view, out int visible);
Guid GetId();
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("AF8DA486-95BB-4460-B3B7-6E7A6B2962B5")]
internal interface IVirtualDesktopManagerInternal {
int GetCount();
void notimpl1(); // void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop);
void notimpl2(); // void CanViewMoveDesktops(IApplicationView view, out int itcan);
IVirtualDesktop GetCurrentDesktop();
void GetDesktops(out IObjectArray desktops);
[PreserveSig]
int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop);
void SwitchDesktop(IVirtualDesktop desktop);
IVirtualDesktop CreateDesktop();
void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback);
IVirtualDesktop FindDesktop(ref Guid desktopid);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
internal interface IVirtualDesktopManager {
int IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow);
Guid GetWindowDesktopId(IntPtr topLevelWindow);
void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("92CA9DCD-5622-4bba-A805-5E9F541BD8C9")]
internal interface IObjectArray {
void GetCount(out int count);
void GetAt(int index, ref Guid iid, [MarshalAs(UnmanagedType.Interface)]out object obj);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("6D5140C1-7436-11CE-8034-00AA006009FA")]
internal interface IServiceProvider10 {
[return: MarshalAs(UnmanagedType.IUnknown)]
object QueryService(ref Guid service, ref Guid riid);
}
}
Next is Desktop.cs. It contains the friendly C# classes that you can use in your code:
using System;
using System.Runtime.InteropServices;
namespace Windows10Interop
{
public class Desktop {
public static int Count {
// Returns the number of desktops
get { return DesktopManager.Manager.GetCount(); }
}
public static Desktop Current {
// Returns current desktop
get { return new Desktop(DesktopManager.Manager.GetCurrentDesktop()); }
}
public static Desktop FromIndex(int index) {
// Create desktop object from index 0..Count-1
return new Desktop(DesktopManager.GetDesktop(index));
}
public static Desktop FromWindow(IntPtr hWnd) {
// Creates desktop object on which window <hWnd> is displayed
Guid id = DesktopManager.WManager.GetWindowDesktopId(hWnd);
return new Desktop(DesktopManager.Manager.FindDesktop(ref id));
}
public static Desktop Create() {
// Create a new desktop
return new Desktop(DesktopManager.Manager.CreateDesktop());
}
public void Remove(Desktop fallback = null) {
// Destroy desktop and switch to <fallback>
var back = fallback == null ? DesktopManager.GetDesktop(0) : fallback.itf;
DesktopManager.Manager.RemoveDesktop(itf, back);
}
public bool IsVisible {
// Returns <true> if this desktop is the current displayed one
get { return object.ReferenceEquals(itf, DesktopManager.Manager.GetCurrentDesktop()); }
}
public void MakeVisible() {
// Make this desktop visible
DesktopManager.Manager.SwitchDesktop(itf);
}
public Desktop Left {
// Returns desktop at the left of this one, null if none
get {
IVirtualDesktop desktop;
int hr = DesktopManager.Manager.GetAdjacentDesktop(itf, 3, out desktop);
if (hr == 0) return new Desktop(desktop);
else return null;
}
}
public Desktop Right {
// Returns desktop at the right of this one, null if none
get {
IVirtualDesktop desktop;
int hr = DesktopManager.Manager.GetAdjacentDesktop(itf, 4, out desktop);
if (hr == 0) return new Desktop(desktop);
else return null;
}
}
public void MoveWindow(IntPtr handle) {
// Move window <handle> to this desktop
DesktopManager.WManager.MoveWindowToDesktop(handle, itf.GetId());
}
public bool HasWindow(IntPtr handle) {
// Returns true if window <handle> is on this desktop
return itf.GetId() == DesktopManager.WManager.GetWindowDesktopId(handle);
}
public override int GetHashCode() {
return itf.GetHashCode();
}
public override bool Equals(object obj) {
var desk = obj as Desktop;
return desk != null && object.ReferenceEquals(this.itf, desk.itf);
}
private IVirtualDesktop itf;
private Desktop(IVirtualDesktop itf) { this.itf = itf; }
}
internal static class DesktopManager {
static DesktopManager() {
var shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(Guids.CLSID_ImmersiveShell));
Manager = (IVirtualDesktopManagerInternal)shell.QueryService(Guids.CLSID_VirtualDesktopManagerInternal, Guids.IID_IVirtualDesktopManagerInternal);
WManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(Guids.CLSID_VirtualDesktopManager));
}
internal static IVirtualDesktop GetDesktop(int index) {
int count = Manager.GetCount();
if (index < 0 || index >= count) throw new ArgumentOutOfRangeException("index");
IObjectArray desktops;
Manager.GetDesktops(out desktops);
object objdesk;
desktops.GetAt(index, Guids.IID_IVirtualDesktop, out objdesk);
Marshal.ReleaseComObject(desktops);
return (IVirtualDesktop)objdesk;
}
internal static IVirtualDesktopManagerInternal Manager;
internal static IVirtualDesktopManager WManager;
}
}
And finally a little test WinForms project that I used to test the code. Just drop 4 buttons on a form and name them buttonLeft/Right/Create/Destroy:
using Windows10Interop;
using System.Diagnostics;
...
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void buttonRight_Click(object sender, EventArgs e) {
var curr = Desktop.FromWindow(this.Handle);
Debug.Assert(curr.Equals(Desktop.Current));
var right = curr.Right;
if (right == null) right = Desktop.FromIndex(0);
if (right != null) {
right.MoveWindow(this.Handle);
right.MakeVisible();
this.BringToFront();
Debug.Assert(right.IsVisible);
}
}
private void buttonLeft_Click(object sender, EventArgs e) {
var curr = Desktop.FromWindow(this.Handle);
Debug.Assert(curr.Equals(Desktop.Current));
var left = curr.Left;
if (left == null) left = Desktop.FromIndex(Desktop.Count - 1);
if (left != null) {
left.MoveWindow(this.Handle);
left.MakeVisible();
this.BringToFront();
Debug.Assert(left.IsVisible);
}
}
private void buttonCreate_Click(object sender, EventArgs e) {
var desk = Desktop.Create();
desk.MoveWindow(this.Handle);
desk.MakeVisible();
Debug.Assert(desk.IsVisible);
Debug.Assert(desk.Equals(Desktop.Current));
}
private void buttonDestroy_Click(object sender, EventArgs e) {
var curr = Desktop.FromWindow(this.Handle);
var next = curr.Left;
if (next == null) next = curr.Right;
if (next != null && next != curr) {
next.MoveWindow(this.Handle);
curr.Remove(next);
Debug.Assert(next.IsVisible);
}
}
}
The only real quirk I noticed while testing this is that moving a window from one desktop to another can move it to the bottom of the Z-order when you first switch the desktop, then move the window. No such problem if you do it the other way around.
There is this guy that made a application to map keyboard shorcut to move a window between virtual desktop.
https://github.com/Grabacr07/SylphyHorn
(I use it every day )
He has a blog where he explain what he did
http://grabacr.net/archives/5701 ( you can use google translate it is in japanese)
He in fact used the same api mantionned in the Alberto Tostado response.
http://www.cyberforum.ru/blogs/105416/blog3671.html
and the api can be found on his github https://github.com/Grabacr07/VirtualDesktop
The api is really simple to use BUT it seems impossible to move a window from another process.
public static bool MoveToDesktop(IntPtr hWnd, VirtualDesktop virtualDesktop)
{
ThrowIfNotSupported();
int processId;
NativeMethods.GetWindowThreadProcessId(hWnd, out processId);
if (Process.GetCurrentProcess().Id == processId) // THAT LINE
{
var guid = virtualDesktop.Id;
VirtualDesktop.ComManager.MoveWindowToDesktop(hWnd, ref guid);
return true;
}
return false;
}
To workaround this problem they made another implementation that they use alongside the one in the russian blog
if (VirtualDesktopHelper.MoveToDesktop(hWnd, right) //<- the one in the russian blog
|| this.helper.MoveWindowToDesktop(hWnd, right.Id)) <- the second implementation
The second implementation can be found here: https://github.com/tmyt/VDMHelper
This one can move a window from another process to another desktop. BUT it is buggy right now. For exemple when i try to move some window like google chrome it crash.
So this is the result of my research. I m rigth now trying to make a StickyWindow feature with these api.
I fear that all about "Virtual desktops" in Windows 10 is undocumented, but in a Russian page I've seen documented the interfaces. I don't speak Russian but seems that they have used reversed engineering. Anyway, the code is very clear (Thanks to them!).
Keep an eye here:
http://www.cyberforum.ru/blogs/105416/blog3671.html
I've been trying to see if the old API's CreateDesktop, OpenDesktop, etc... is linked to the new Virtual-Desktops, but no way...
The interfaces work with the final production release of Windows 10 (2015-05-08), but you shouldn't use them in a real wide distributed application until Microsoft documents them. Too much risk.
Regards.
I would like to create a function in C# that takes a specific webpage and coverts it to a JPG image from within ASP.NET. I don't want to do this via a third party or thumbnail service as I need the full image. I assume I would need to somehow leverage the webbrowser control from within ASP.NET but I just can't see where to get started. Does anyone have examples?
Ok, this was rather easy when I combined several different solutions:
These solutions gave me a thread-safe way to use the WebBrowser from ASP.NET:
http://www.beansoftware.com/ASP.NET-Tutorials/Get-Web-Site-Thumbnail-Image.aspx
http://www.eggheadcafe.com/tutorials/aspnet/b7cce396-e2b3-42d7-9571-cdc4eb38f3c1/build-a-selfcaching-asp.aspx
This solution gave me a way to convert BMP to JPG:
Bmp to jpg/png in C#
I simply adapted the code and put the following into a .cs:
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Windows.Forms;
public class WebsiteToImage
{
private Bitmap m_Bitmap;
private string m_Url;
private string m_FileName = string.Empty;
public WebsiteToImage(string url)
{
// Without file
m_Url = url;
}
public WebsiteToImage(string url, string fileName)
{
// With file
m_Url = url;
m_FileName = fileName;
}
public Bitmap Generate()
{
// Thread
var m_thread = new Thread(_Generate);
m_thread.SetApartmentState(ApartmentState.STA);
m_thread.Start();
m_thread.Join();
return m_Bitmap;
}
private void _Generate()
{
var browser = new WebBrowser { ScrollBarsEnabled = false };
browser.Navigate(m_Url);
browser.DocumentCompleted += WebBrowser_DocumentCompleted;
while (browser.ReadyState != WebBrowserReadyState.Complete)
{
Application.DoEvents();
}
browser.Dispose();
}
private void WebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
// Capture
var browser = (WebBrowser)sender;
browser.ClientSize = new Size(browser.Document.Body.ScrollRectangle.Width, browser.Document.Body.ScrollRectangle.Bottom);
browser.ScrollBarsEnabled = false;
m_Bitmap = new Bitmap(browser.Document.Body.ScrollRectangle.Width, browser.Document.Body.ScrollRectangle.Bottom);
browser.BringToFront();
browser.DrawToBitmap(m_Bitmap, browser.Bounds);
// Save as file?
if (m_FileName.Length > 0)
{
// Save
m_Bitmap.SaveJPG100(m_FileName);
}
}
}
public static class BitmapExtensions
{
public static void SaveJPG100(this Bitmap bmp, string filename)
{
var encoderParameters = new EncoderParameters(1);
encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
bmp.Save(filename, GetEncoder(ImageFormat.Jpeg), encoderParameters);
}
public static void SaveJPG100(this Bitmap bmp, Stream stream)
{
var encoderParameters = new EncoderParameters(1);
encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
bmp.Save(stream, GetEncoder(ImageFormat.Jpeg), encoderParameters);
}
public static ImageCodecInfo GetEncoder(ImageFormat format)
{
var codecs = ImageCodecInfo.GetImageDecoders();
foreach (var codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
// Return
return null;
}
}
And can call it as follows:
WebsiteToImage websiteToImage = new WebsiteToImage( "http://www.cnn.com", #"C:\Some Folder\Test.jpg");
websiteToImage.Generate();
It works with both a file and a stream. Make sure you add a reference to System.Windows.Forms to your ASP.NET project. I hope this helps.
UPDATE: I've updated the code to include the ability to capture the full page and not require any special settings to capture only a part of it.
Good solution by Mr Cat Man Do.
I've needed to add a row to suppress some errors that came up in some webpages
(with the help of an awesome colleague of mine)
private void _Generate()
{
var browser = new WebBrowser { ScrollBarsEnabled = false };
browser.ScriptErrorsSuppressed = true; // <--
browser.Navigate(m_Url);
browser.DocumentCompleted += WebBrowser_DocumentCompleted;
}
...
Thanks Mr Do
Here is my implementation using extension methods and task factory instead thread:
/// <summary>
/// Convert url to bitmap byte array
/// </summary>
/// <param name="url">Url to browse</param>
/// <param name="width">width of page (if page contains frame, you need to pass this params)</param>
/// <param name="height">heigth of page (if page contains frame, you need to pass this params)</param>
/// <param name="htmlToManipulate">function to manipulate dom</param>
/// <param name="timeout">in milliseconds, how long can you wait for page response?</param>
/// <returns>bitmap byte[]</returns>
/// <example>
/// byte[] img = new Uri("http://www.uol.com.br").ToImage();
/// </example>
public static byte[] ToImage(this Uri url, int? width = null, int? height = null, Action<HtmlDocument> htmlToManipulate = null, int timeout = -1)
{
byte[] toReturn = null;
Task tsk = Task.Factory.StartNew(() =>
{
WebBrowser browser = new WebBrowser() { ScrollBarsEnabled = false };
browser.Navigate(url);
browser.DocumentCompleted += (s, e) =>
{
var browserSender = (WebBrowser)s;
if (browserSender.ReadyState == WebBrowserReadyState.Complete)
{
if (htmlToManipulate != null) htmlToManipulate(browserSender.Document);
browserSender.ClientSize = new Size(width ?? browser.Document.Body.ScrollRectangle.Width, height ?? browser.Document.Body.ScrollRectangle.Bottom);
browserSender.ScrollBarsEnabled = false;
browserSender.BringToFront();
using (Bitmap bmp = new Bitmap(browserSender.Document.Body.ScrollRectangle.Width, browserSender.Document.Body.ScrollRectangle.Bottom))
{
browserSender.DrawToBitmap(bmp, browserSender.Bounds);
toReturn = (byte[])new ImageConverter().ConvertTo(bmp, typeof(byte[]));
}
}
};
while (browser.ReadyState != WebBrowserReadyState.Complete)
{
Application.DoEvents();
}
browser.Dispose();
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
tsk.Wait(timeout);
return toReturn;
}
There is a good article by Peter Bromberg on this subject here. His solution seems to do what you need...
The solution is perfect, just needs a fixation in the line which sets the WIDTH of the image. For pages with a LARGE HEIGHT, it does not set the WIDTH appropriately:
//browser.ClientSize = new Size(browser.Document.Body.ScrollRectangle.Width, browser.Document.Body.ScrollRectangle.Bottom);
browser.ClientSize = new Size(1000, browser.Document.Body.ScrollRectangle.Bottom);
And for adding a reference to System.Windows.Forms, you should do it in .NET-tab of ADD REFERENCE instead of COM -tab.
You could use WatiN to open a new browser, then capture the screen and crop it appropriately.